Interpreting OfficerRights registry value

Dec 7, 2015 at 9:07 AM

I needed to test whether the current logged on account had "certificate manager" rights on a given template (linked to issue/feature request #61). Testing whether the account has global rights is easy but as soon as certificate managers restrictions are applied it gets quite tricky... (we're using restrictions so that our less privileged/experienced PKI operators only have rights on the less sensitive templates).

I figured that those restrictions are configured in the OfficerRights registry value and using certutil -getreg on it displays the restrictions but I'd like to avoid parsing certutil's output...
Using the CertAdmin COM interface (I'm doing it remotely), I've been able to get the binary value of the registry value but I don't know how to convert this byte array into something usable.

Could you please give me a hint on how to format this byte array into some usable structure?
Thank you,
Dec 7, 2015 at 11:52 AM
Unfortunately, at this point I can't help you here. A quick observation of the officer rights reg entry suggested me that this is a standard security descriptor. However, standard tools/API do not return certificate template mappings for enrollment agents. At least, certutil on Windows Server 2008 (the minimum OS version I have to support) do not provide such information. Example output in my case is:
  OfficerRights REG_BINARY =
    Deny        BUILTIN\Administrators
    Deny        BUILTIN\Administrators
    Deny        CONTOSO\Domain Admins
    Deny        CONTOSO\Enterprise Admins
Can you confirm if the output is similar to the one you see? Currently, I'm at vacation and don't have access to other OS versions, except my lab CA and your information would help me to identify possible ways to get this information.

By the end, it may be necessary to write security descriptor decoder for this application from scratch (to retrieve certificate template mappings).
Dec 7, 2015 at 2:33 PM
Don't worry there is nothing urgent.
Note that my CA is running on a Windows Server 2008 R2 ADCS.
Running certutil from my Windows 8.1 computer returns the following output (I have replaced the domain and group names):
 OfficerRights REG_BINARY =
    Allow Everyone      TEST\GROUP-RESTRICTED
    Allow Everyone      TEST\GROUP-RESTRICTED
    Allow Everyone      TEST\GROUP-RESTRICTED
    Allow Everyone      TEST\GROUP-RESTRICTED
    Allow Everyone      TEST\GROUP-RESTRICTED
    Allow Everyone      TEST\GROUP-RESTRICTED
    Allow Everyone      TEST\GROUP-PRIVILEGED
Dec 7, 2015 at 2:40 PM
It appears that certutil was updated since Windows Server 2008. This means that it will be necessary to manually decode security descriptor. I will think about this. In the case if I suecceed in this, I will post updates to my weblog first.
Dec 8, 2015 at 5:58 PM
The task was pretty challenging.

After diving into actual data of OfficerRights registry value, I realized that although this structure is standard security descriptor, it uses specific access control entries (ACE) and no common security descriptor is able to show additional data.

OfficerRights uses ACCESS_ALLOWED_CALLBACK_ACE and ACCESS_DENIED_CALLBACK_ACE which are defined in [MS-DTYP] § and §, respectively.

These ACE types use application-specific data behind SID information. Exact semantics of this application-specific data is specified in the [MS-CSRA] § Note, that this page contains typos: AccessMask field is 0x00010000 (instead of 0x0001000). The rest is more or less straightforward.

I have managed to write PoC of the decoder which decodes OfficerRights byte array to some meaningful structure.

Please, note, this is PoC. It is inefficient, do not provide error checks and wasn't heavily tested. I just tested several combinations on my lab server and it works for me. This may not be true for you. But you can try.
function Convert-OfficerRights ([Byte[]]$bytes) {
    function Convert-BinaryToSid([Byte[]]$SidBytes) {
        $SidString = "S-" + $SidBytes[0]
        $SidLength = if ($SidBytes[1] -lt 1) {12} else {12 + ($SidBytes[1] - 1) * 4}
        $IdentifierAuthorityBytes = (0,0) + $SidBytes[2..7]
        $IdentifierAuthority = [BitConverter]::ToUInt64($IdentifierAuthorityBytes, 0)
        $SidString += "-" + $IdentifierAuthority
        $IdentifierAuthorityDec = [BitConverter]::ToUInt32($SidBytes[8..11],0)
        $SidString += "-" + $IdentifierAuthorityDec
        [Byte[]]$rest = $SidBytes[12..($SidLength)]
        $ExpectedBytes = ($SidBytes[1] - 1) * 4
        if ($ExpectedBytes -ne $rest.Length) {
            throw "The value is invalid"
        for ($index = 12; $index -lt $rest.Length + 12; $index += 4) {
            $tokenBytes = $SidBytes[$index..($index + 3)]
            $SidString += "-" + [BitConverter]::ToUInt32($tokenBytes, 0)
    $Revision = $bytes[0]
    $Sbz1 = $bytes[1]
    $control = $bytes[2..3]
    $OffsetOwner = [BitConverter]::ToInt32($bytes[4..7],0)
    $OffsetGroup = [BitConverter]::ToInt32($bytes[8..11],0)
    $OffsetSacl = [BitConverter]::ToInt32($bytes[12..15],0)
    $OffsetDacl = [BitConverter]::ToInt32($bytes[16..19],0)
    $SidLength = if ($bytes[$OffsetOwner + 1] -lt 1) {12}
    else {12 + ($bytes[$OffsetOwner + 1] - 1) * 4}
    $OwnerSid = $bytes[$OffsetOwner..($OffsetOwner + $SidLength)]
    #region Read DACL
    $AclRevision = $bytes[$OffsetDacl]
    $AclPadding = $bytes[$OffsetDacl + 1]
    $AclSize = [BitConverter]::ToInt16($bytes[($OffsetDacl + 2)..($OffsetDacl + 3)],0)
    $AceCount = [BitConverter]::ToInt16($bytes[($OffsetDacl + 4)..($OffsetDacl + 5)],0)
    $AcePadding = [BitConverter]::ToInt16($bytes[($OffsetDacl + 6)..($OffsetDacl + 7)],0)
    $AceStart = $OffsetDacl + 8 + $AcePadding
    $Aces = @()
    for ($index = 0; $index -lt $AceCount; $index++) {
        $AceObject = New-Object psobject -Property @{
            AceType = $null
            Flags = $null
            Size = $null
            AccessMask = $null
            OfficerSID = $null
            OfficerName = $null
            OID = $null
            TargetSID = @()
        $AceObject.AceType = if ($bytes[$AceStart] -eq 9) {"Allow"}
        elseif ($bytes[$AceStart] -eq 10) {"Deny"}
        else {"Unknown"}
        $AceObject.Flags = $bytes[$AceStart + 1]
        $AceObject.Size = [BitConverter]::ToUInt16($bytes[($AceStart + 2)..($AceStart + 3)],0)
        $AceMaskBytes = $bytes[($AceStart + 4)..($AceStart + 7)]
        $AceObject.AccessMask = [BitConverter]::ToUInt32($bytes[($AceStart + 4)..($AceStart + 7)],0)
        $SidLength = if ($bytes[$AceStart + 9] -lt 1) {12} else {12 + ($bytes[$AceStart + 9] - 1) * 4}
        $AceObject.OfficerSID = Convert-BinaryToSid $bytes[($AceStart + 8)..($AceStart + 7 + $SidLength)]
        try {
            $AceObject.OfficerName = ((new-object security.principal.securityidentifier $AceObject.OfficerSID).translate([security.principal.ntaccount])).Value
        } catch { }
        $OffsetOid = $AceStart + 8 + $SidLength
        $SidCount = [BitConverter]::ToUInt32($bytes[$OffsetOid..($OffsetOid + 3)],0)
        for ($i = 0; $i -lt $SidCount; $i++) {
            $SidLength = if ($bytes[$OffsetOid + 5] -le 1) {12} else {12 + ($bytes[$OffsetOid + 5] - 1) * 4}
            $SidBytes = $bytes[($OffsetOid + 4)..($OffsetOid + 3 + $SidLength)]
            $AceObject.TargetSID += Convert-BinaryToSid $SidBytes
            $OffsetOid += $SidLength
        $OidBytes = $bytes[($OffsetOid + 4)..($AceStart + $AceObject.Size - 1)]
        $AceObject.OID = [Security.Cryptography.Oid][Text.Encoding]::Unicode.GetString($OidBytes).TrimEnd()
        $AceStart += $AceObject.Size
        $Aces += $AceObject
The usage is simple, just pass byte array (returned by ICertAdmin2::GetConfigEntry) to this function: Convert-OfficerRights $byteArray.

Output is a collection of ACEs. Each ACE contains the following data:

Size -- size of the ACE in bytes
TargetSID - An array of security identifiers to which enrollment agent is allowed to issue certificates (bottom box in the corresponding UI)
AceType - Specifies the type of this ACE: allow or deny (bottom box in the corresponding UI)
OID - object identifier of the certificate template.
Flags - constant
OfficerSID - enrollment agent's security identifier.
AccessMask - constant
OfficerName - resolved enrollment agent's name.
Dec 9, 2015 at 7:36 AM

Well it works like a charm in my case! Thank you very much for this and for the reactivity!

I haven't tested it on complex scenarios (for instance I never restrict the targetSid so it is always just Everyone) but for my use case at least it is perfectly usable.
There's only the case where there are no template restrictions where we might expect OID to be $null instead of a blank OID but that's a detail.

Thanks again and have a nice day,
Dec 30, 2015 at 5:19 AM
Marked as answer by jalliot on 3/14/2016 at 9:00 AM