AD CS: PKISync.ps1 Script for Cross-forest Certificate Enrollment

Applies To: Windows Server 2008 R2, Windows Server 2008 R2 with SP1

PKISync.ps1 copies objects in the source forest to the target forest.

In cross-forest AD CS deployments, use PKISync.ps1 during initial deployment and to keep resource and account forest PKI objects synchronized.

Saving PKISync.ps1

To save PKISync.ps1 to a file

  1. Click Copy Code at the top of the code section.

  2. Start Notepad.

  3. On the Edit menu, click Paste.

  4. On the File menu, click Save.

  5. Type a path for the file, type the file name PKISync.ps1, and click Save.

# This script allows updating PKI objects in Active Directory for the 
# cross-forest certificate enrollment
#This sample script is not supported under any Microsoft standard support 
#program or service. This sample script is provided AS IS without warranty of 
#any kind. Microsoft further disclaims all implied warranties including,
#without limitation, any implied warranties of merchantability or of fitness
#for a particular purpose. The entire risk arising out of the use or 
#performance of the sample scripts and documentation remains with you. In no 
#event shall Microsoft, its authors, or anyone else involved in the creation,
#production, or delivery of the scripts be liable for any damages whatsoever 
# (including, without limitation, damages for loss of business profits, business
#interruption, loss of business information, or other pecuniary loss) arising 
#out of the use of or inability to use this sample script or documentation, 
#even if Microsoft has been advised of the possibility of such damages.

# Command line variables
$SourceForestName = ""
$TargetForestName = ""
$SourceDC = ""
$TargetDC = ""

$ObjectType = "all"
$ObjectCN = $null

$DryRun = $FALSE
$DeleteOnly = $FALSE
$OverWrite = $FALSE

function ParseCommandLine()
{
    if (2 -gt $Script:args.Count)
    {
        write-warning "Not enough arguments"
        Usage 
        exit 87
    }

    for($i = 0; $i -lt $Script:args.Count; $i++)
    {
        switch($Script:args[$i].ToLower())
        {
            -sourceforest 
            {
                $i++
                $Script:SourceForestName = $Script:args[$i]
            }
            -targetforest 
            {
                $i++
                $Script:TargetForestName = $Script:args[$i]
            }                 
            -cn 
            {
                $i++
                $Script:ObjectCN = $Script:args[$i]
            }
            -type 
            {
                $i++
                $Script:ObjectType = $Script:args[$i].ToLower()
            }
            -f 
            {
                $Script:OverWrite = $TRUE
            }
            -whatif
            {
                $Script:DryRun = $TRUE
            }
            -deleteOnly
            {
                $Script:DeleteOnly = $TRUE
            }
            -targetdc
            {
                $i++
                $Script:TargetDC = $Script:args[$i]
            }
            -sourcedc
            {
                $i++
                $Script:SourceDC = $Script:args[$i]
            }
            default 
            {
                write-warning ("Unknown parameter: " + $Script:args[$i])
                Usage
                exit 87
            }
        }
    }
}

function Usage()
{
    write-host ""
    write-host "Script to copy or delete PKI objects (default is copy)"
    write-host ""
    write-host "  Copy Command:"
    write-host ""
    write-host "  .\PKISync.ps1 -sourceforest <SourceForestDNS> -targetforest <TargetForestDNS> [-sourceDC <SourceDCDNS>] [-targetDC <TargetDCDNS>] [-type <CA|Template|OID> [-cn <ObjectCN>]] [-f] [-whatif]"
    write-host ""
    write-host "  Delete Command:"
    write-host ""
    write-host "  .\PKISync.ps1 -targetforest <TargetForestDNS> [-targetDC <TargetDCDNS>] [-type <CA|Template|OID> [-cn <ObjectCN>]] [-deleteOnly] [-whatif]"
    write-host ""
    write-host "-sourceforest           -- DNS of the forest to process object from"
    write-host "-targetforest           -- DNS of the forest to process object to"
    write-host "-sourcedc               -- DNS of the DC in the source forest to process object from"
    write-host "-targetdc               -- DNS of the DC in the target forest to process object to"
    write-host "-type                   -- Type of object to process, if omitted then all object types are processed"
    write-host "                           CA         -- Process CA object(s)"
    write-host "                           Template   -- Process Template object(s)"
    write-host "                           OID        -- Process OID object(s)"
    write-host '-cn                     -- Common name of the object to process, do not include the cn= (ie "User" and not "CN=User"'
    write-host "                           This option is only valid if -type <> is also specified"
    write-host "-f                      -- Force overwrite of existing objects when copying. Ignored when deleting."
    write-host "-whatif                 -- Display what object(s) will be processed without processing"
    write-host "-deleteOnly             -- Will delete object in the target forest if it exists"            
    write-host ""
    write-host ""    
}

# Build a list of attributes to copy for some object type
function GetSchemaSystemMayContain($ForestContext, $ObjectType)
{
    # first get all attributes that are part of systemMayContain list
    $SchemaDE = [System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass]::FindByName($ForestContext, $ObjectType).GetDirectoryEntry()
    $SystemMayContain = $SchemaDE.systemMayContain

    # if schema was upgraded with adprep.exe, we need to check mayContain list as well
    if($null -ne $SchemaDE.mayContain)
    {
        $MayContain = $SchemaDE.mayContain
        foreach($attr in $MayContain)
        {
            $SystemMayContain.Add($attr)
        }
    }

    # special case some of the inherited attributes
    if (-1 -eq $SystemMayContain.IndexOf("displayName"))
    {
        $SystemMayContain.Add("displayName")
    }
    if (-1 -eq $SystemMayContain.IndexOf("flags"))
    {
        $SystemMayContain.Add("flags")
    }
    if ($objectType.ToLower().Contains("template") -and -1 -eq $SystemMayContain.IndexOf("revision"))
    {
        $SystemMayContain.Add("revision")
    }

    return $SystemMayContain
}

# Copy or delete all objects of some type
function ProcessAllObjects($SourcePKIServicesDE, $TargetPKIServicesDE, $RelativeDN)
{
    $SourceObjectsDE = $SourcePKIServicesDE.psbase.get_Children().find($RelativeDN)
    $ObjectCN = $null

    foreach($ChildNode in $SourceObjectsDE.psbase.get_Children())
    {
        # if some object failed, we will try to continue with the rest
        trap
        {
            # CN maybe null here, but its ok. Doing best effort. 
            write-warning ("Error while coping an object. CN=" + $ObjectCN)
            write-warning $_
            write-warning $_.InvocationInfo.PositionMessage
            continue
        }

        $ObjectCN = $ChildNode.psbase.Properties["cn"]
        ProcessObject $SourcePKIServicesDE $TargetPKIServicesDE $RelativeDN $ObjectCN
        $ObjectCN = $null
    }

}

# Copy or delete an object
function ProcessObject($SourcePKIServicesDE, $TargetPKIServicesDE, $RelativeDN, $ObjectCN)
{
    $SourceObjectContainerDE = $SourcePKIServicesDE.psbase.get_Children().find($RelativeDN)
    $TargetObjectContainerDE = $TargetPKIServicesDE.psbase.get_Children().find($RelativeDN)

    # when copying make sure there is an object to copy
    if($FALSE -eq $Script:DeleteOnly)
    {
        $DSSearcher =  [System.DirectoryServices.DirectorySearcher]$SourceObjectContainerDE
        $DSSearcher.Filter = "(cn=" +$ObjectCN+")"
        $SearchResult = $DSSearcher.FindAll()
        if (0 -eq $SearchResult.Count)
        {
            write-host ("Source object does not exist: CN=" + $ObjectCN + "," + $RelativeDN)
            return
        }
        $SourceObjectDE = $SourceObjectContainerDE.psbase.get_Children().find("CN=" + $ObjectCN)
    }

    # Check to see if the target object exists, if it does delete if overwrite is enabled.
    # Also delete is this a deletion only operation.
    $DSSearcher =  [System.DirectoryServices.DirectorySearcher]$TargetObjectContainerDE
    $DSSearcher.Filter = "(cn=" +$ObjectCN+")"
    $SearchResult = $DSSearcher.FindAll()
    if ($SearchResult.Count -gt 0)
    {
        $TargetObjectDE = $TargetObjectContainerDE.psbase.get_Children().find("CN=" + $ObjectCN)

        if($Script:DeleteOnly) 
        {
            write-host ("Deleting: " + $TargetObjectDE.DistinguishedName)
            if($FALSE -eq $DryRun)
            {
                $TargetObjectContainerDE.psbase.get_Children().Remove($TargetObjectDE)
            }
            return
        } 
        elseif ($Script:OverWrite)
        {
            write-host ("OverWriting: " + $TargetObjectDE.DistinguishedName)
            if($FALSE -eq $DryRun)
            {
                $TargetObjectContainerDE.psbase.get_Children().Remove($TargetObjectDE)
            }
        } 
        else 
        {
            write-warning ("Object exists, use -f to overwrite. Object: " + $TargetObjectDE.DistinguishedName)
            return
        }
    } 
    else 
    {
        if($Script:DeleteOnly) 
        {
            write-warning ("Can't delete object. Object doesn't exist. Object: " + $ObjectCN + ", " + $TargetObjectContainerDE.DistinguishedName)
            return
        } 
        else 
        {        
            write-host ("Copying Object: " + $SourceObjectDE.DistinguishedName)
        }
    }

    # Only update the object if this is not a dry run
    if($FALSE -eq $DryRun -and $FALSE -eq $Script:DeleteOnly)
    {
        #Create new AD object   
        $NewDE = $TargetObjectContainerDE.psbase.get_Children().Add("CN=" + $ObjectCN, $SourceObjectDE.psbase.SchemaClassName)

        #Obtain systemMayContain for the object type from the AD schema
        $ObjectMayContain = GetSchemaSystemMayContain $SourceForestContext $SourceObjectDE.psbase.SchemaClassName
        #Copy attributes defined in the systemMayContain for the object type
        foreach($Attribute in $ObjectMayContain)
        {
            $AttributeValue = $SourceObjectDE.psbase.Properties[$Attribute].Value
            if ($null -ne $AttributeValue)
            {
                $NewDE.psbase.Properties[$Attribute].Value = $AttributeValue
                $NewDE.psbase.CommitChanges()
            }
        }
        #Copy secuirty descriptor to new object. Only DACL is copied. 
        $BinarySecurityDescriptor = $SourceObjectDE.psbase.ObjectSecurity.GetSecurityDescriptorBinaryForm()
        $NewDE.psbase.ObjectSecurity.SetSecurityDescriptorBinaryForm($BinarySecurityDescriptor, [System.Security.AccessControl.AccessControlSections]::Access)
        $NewDE.psbase.CommitChanges()
    }
}

# Get parent container for all PKI objects in the AD
function GetPKIServicesContainer([System.DirectoryServices.ActiveDirectory.DirectoryContext] $ForestContext, $dcName)
{
    $ForObj = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext)
    $DE = $ForObj.RootDomain.GetDirectoryEntry()

    if("" -ne $dcName)
    {
        $newPath = [System.Text.RegularExpressions.Regex]::Replace($DE.psbase.Path, "LDAP://\S*/", "LDAP://" + $dcName + "/")
        $DE = New-Object System.DirectoryServices.DirectoryEntry $newPath 
    } 

    $PKIServicesContainer = $DE.psbase.get_Children().find("CN=Public Key Services,CN=Services,CN=Configuration")
    return $PKIServicesContainer
}

#########################################################
# Main script code
#########################################################

# All errors are fatal by default unless there is another 'trap' with 'continue'
trap
{
    write-error "The script has encoutnered a fatal error. Terminating script."
    break
}

ParseCommandLine

# Get a hold of the containers in each forest
write-host ("Target Forest: " + $TargetForestName.ToUpper())
$TargetForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext Forest, $TargetForestName
$TargetPKIServicesDE = GetPKIServicesContainer $TargetForestContext $Script:TargetDC

# Only need source forest when copying
if($FALSE -eq $Script:DeleteOnly)
{
    write-host ("Source Forest: " + $SourceForestName.ToUpper())
    $SourceForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext Forest, $SourceForestName
    $SourcePKIServicesDE = GetPKIServicesContainer $SourceForestContext $Script:SourceDC
} 
else 
{
    $SourcePKIServicesDE = $TargetPKIServicesDE
}

if("" -ne $ObjectType) {write-host ("Object Category to process: " + $ObjectType.ToUpper())}

# Process the command
switch($ObjectType.ToLower())
{
    all
    {
        write-host ("Enrollment Serverices Container")
    ProcessAllObjects $SourcePKIServicesDE $TargetPKIServicesDE "CN=Enrollment Services"
        write-host ("Certificate Templates Container")
        ProcessAllObjects $SourcePKIServicesDE $TargetPKIServicesDE "CN=Certificate Templates"
        write-host ("OID Container")
        ProcessAllObjects $SourcePKIServicesDE $TargetPKIServicesDE "CN=OID"
    }
    ca
    {
        if($null -eq $ObjectCN)
        {
            ProcessAllObjects $SourcePKIServicesDE $TargetPKIServicesDE "CN=Enrollment Services"
        } 
        else 
        {
            ProcessObject $SourcePKIServicesDE $TargetPKIServicesDE "CN=Enrollment Services" $ObjectCN
        }
    }
    oid
    {
        if($null -eq $ObjectCN)
        {
            ProcessAllObjects $SourcePKIServicesDE $TargetPKIServicesDE "CN=OID"
        } 
        else 
        {
            ProcessObject $SourcePKIServicesDE $TargetPKIServicesDE "CN=OID" $ObjectCN
        }
    }
    template
    {
        if($null -eq $ObjectCN)
        {
            ProcessAllObjects $SourcePKIServicesDE $TargetPKIServicesDE "CN=Certificate Templates"
        } 
        else 
        {
            ProcessObject $SourcePKIServicesDE $TargetPKIServicesDE "CN=Certificate Templates" $ObjectCN
        }    
    }
    default
    {
        write-warning ("Unknown object type: " + $ObjectType.ToLower())
        Usage
        exit 87
    }
} 

Subsection Heading

Insert subsection body here.