Private/ConvertTo-NtdsSidExtension.ps1

function ConvertTo-NtdsSidExtension {
    <#
        .SYNOPSIS
        Builds the szOID_NTDS_CA_SECURITY_EXT extension value for a given SID.
 
        .DESCRIPTION
        Encodes the NTDS CA security extension (OID 1.3.6.1.4.1.311.25.2) value bytes.
        This extension carries the target account's objectSid so the CA embeds it in the
        issued certificate, enabling strong certificate-to-account mapping on Server 2025
        KDCs (KB5014754).
 
        The value is a DER-encoded structure (matching Certify's EncodeSidExtension):
 
            ExtensionValue ::= SEQUENCE { -- OtherName wrap
                type-id OBJECT IDENTIFIER (1.3.6.1.4.1.311.25.2.1 -- szOID_NTDS_OBJECTSID),
                value [0] OCTET STRING (ASCII SID string, e.g. "S-1-5-21-...-500")
            }
 
        Specifically the bytes are (Certify EncodeSidExtension layout):
            SEQUENCE {
              [0] EXPLICIT { -- context tag 0, constructed (OtherName content, no inner SEQUENCE)
                OID 1.3.6.1.4.1.311.25.2.1,
                [0] EXPLICIT { OCTET STRING (sid ascii) }
              }
            }
 
        .PARAMETER Sid
        The SID string of the target account (e.g. 'S-1-5-21-...-500').
 
        .OUTPUTS
        System.Byte[] — DER bytes for the X509Extension RawData.
 
        .NOTES
        Format reference: MS-WCCE szOID_NTDS_CA_SECURITY_EXT. Matches Certify's
        CertSidExtension.EncodeSidExtension() output.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern('^S-\d+-\d+(-\d+)+$')]
        [string]$Sid
    )

    $sidBytes = [System.Text.Encoding]::ASCII.GetBytes($Sid)

    # Minimal DER writer helpers
    function Write-Len([int]$len) {
        if ($len -lt 0x80) { return [byte[]]@([byte]$len) }
        if ($len -lt 0x100) { return [byte[]]@(0x81, [byte]$len) }
        return [byte[]]@(0x82, [byte]($len -shr 8), [byte]($len -band 0xFF))
    }
    function Add-Element([System.Collections.Generic.List[byte]]$buf, [byte]$tag, [byte[]]$val) {
        $buf.Add($tag)
        $buf.AddRange([byte[]](Write-Len $val.Length))
        $buf.AddRange([byte[]]$val)
    }

    # OID 1.3.6.1.4.1.311.25.2.1 encoded bytes
    $oidBytes = [byte[]]@(0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x19, 0x02, 0x01)

    # Inner OCTET STRING wrapping the SID ascii: 04 <len> <sid>
    $sidOctet = [System.Collections.Generic.List[byte]]::new()
    Add-Element $sidOctet 0x04 $sidBytes

    # [0] EXPLICIT around the octet string: A0 <len> <sidOctet>
    $explicitOctet = [System.Collections.Generic.List[byte]]::new()
    Add-Element $explicitOctet 0xA0 $sidOctet.ToArray()

    # [0] EXPLICIT OtherName content: OID (type-id) + [0] EXPLICIT OCTET STRING (value).
    # Certify's EncodeSidExtension puts the OID and value DIRECTLY inside the outer [0],
    # with no intermediate SEQUENCE. (A previous version wrapped them in 0x30 SEQUENCE,
    # producing a 2-byte-longer value the KDC's strong-mapping parser rejected with
    # KRB-ERROR 60.)
    $otherNameContent = [System.Collections.Generic.List[byte]]::new()
    Add-Element $otherNameContent 0x06 $oidBytes
    $otherNameContent.AddRange([byte[]]$explicitOctet.ToArray())

    # Outer [0] EXPLICIT wraps the OtherName content directly.
    $outerExplicit = [System.Collections.Generic.List[byte]]::new()
    Add-Element $outerExplicit 0xA0 $otherNameContent.ToArray()

    # Outer SEQUENCE wrapping the OtherName
    $outerSeq = [System.Collections.Generic.List[byte]]::new()
    Add-Element $outerSeq 0x30 $outerExplicit.ToArray()

    return $outerSeq.ToArray()
}