Hi, PowerShell CryptoGuy is here again.

I'm intensively working on the PowerShell PKI Module development and functionality expansion and I want to talk about recent issue. In next version Get-CertificateTemplate will expose template settings which you can look in Certificate Templates MMC snap-in UI. This includes certificate validity, renewal periods, key generation options, issuance requirements, extensions and so on. In other words, everything else that may have sense for PKI administrators.

While almost everything was very easy to implement, I was struggled with pKIExpirationPeriod and pKIOverlapPeriod attributes in Active Directory. The problem is that documentation states that the value is FILETIME structure. And this structure values starts with 1601 year. FILETIME structure can be transformed to a single long (as type) integer rather than as 2 unsigned integers. I've done the same trick previously in this article: How to convert ms-PKI-Roaming-TimeStamp attribute. However this trick won't work in this case. Some investigations. The following value we can see for default Web Server template in ADSIEdit.msc:

Certificate template validity in ADSI

When you look in UI, you will see that certificates issued based on Web Server template are valid for 2 years:

Certificate template validity in Certificate Templates MMC UI

I've tried various (some of them were really crazy) ways to convert the octet array to get desired 2 years. For example, I've tried to transform this array to a FILETIME structure and marshal it to long integer and through .NET DateTime class convert to a DateTime object. The crazy ways were with SYSTEMTIME. In overall, 2 days with no luck. After then I opened Google (Bing too frequently fails to find anything helpful for me)! And Google had only 1 link for me (besides over9000 MS documentation copypastes on various dumb resources). The most interesting thing was that an article was written by another PowerShell MVP (yeah, PowerShell MVPs are the real Power!!!111oneone) Michal Gajda from Poland and his article: Konwersja PKI-Expiration-Period. I highly recommend to get a translator and read the post. I never thought that it was so easy: reverse octet array and you get the timespan with 100 nanoseconds precision. Divide this value to get seconds and then continue division operation unless you get something that makes sense.

His solution is too straightforward, but works very well. The only thing is that you really don't need to use loops to reverse arrays and convert each byte to a hex octet, because entire loop and byte conversion is made with 2 lines:

[array]::Reverse($ByteArray)
$LittleEndianByte = -join ($ByteArray | %{"{0:x2}" -f $_})

In addition I used calculations (as shown under the script) to get exact values as you see in UI. Say, not 365 days, or 12 months, but 1 year. To implement this logic I used the most significant period unit (years) and checked whether the resulting value is divided by the second count in year without the remainder and is greater or equals to 1 (to hit the highest unit). Here is a justified script:

function Convert-pKIPeriod ([Byte[]]$ByteArray) {
    [array]::Reverse($ByteArray)
    $LittleEndianByte = -join ($ByteArray | %{"{0:x2}" -f $_})
    $Value = [Convert]::ToInt64($LittleEndianByte,16) * -.0000001
    if (!($Value % 31536000) -and ($Value / 31536000) -ge 1) {[string]($Value / 31536000) + " years"}
    elseif (!($Value % 2592000) -and ($Value / 2592000) -ge 1) {[string]($Value / 2592000) + " months"}
    elseif (!($Value % 604800) -and ($Value / 604800) -ge 1) {[string]($Value / 604800) + " weeks"}
    elseif (!($Value % 86400) -and ($Value / 86400) -ge 1) {[string]($Value / 86400) + " days"}
    elseif (!($Value % 3600) -and ($Value / 3600) -ge 1) {[string]($Value / 3600) + " hours"}
    else {"0 hours"}
}

in a fact, I could get value not in seconds, but in hours (as hour — is the least period unit supported by certificate template), however it doesn't matter at this point. And what we have:

PS C:\> $adsi = [adsi]("LDAP://" + (Get-CertificateTemplate -Name webserver).dn)
PS C:\> $adsi


distinguishedName : {CN=WebServer,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=conto
                    so,DC=com}
Path              : LDAP://CN=WebServer,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC
                    =contoso,DC=com



PS C:\> $adsi.Properties["pKIExpirationPeriod"].Value
0
128
114
14
93
194
253
255
PS C:\> Convert-pKIPeriod $adsi.Properties["pKIExpirationPeriod"].Value
2 years
PS C:\> Convert-pKIPeriod $adsi.Properties["pKIOverlapPeriod"].Value
6 weeks
PS C:\>

And here we go! I got exact values for validity and certificate renewal periods.

As the epilogue: in certain cases the things are much easier than they looks.

Good luck!


Share this article:

Comments:

kirill

public static class PkiPeriod { public static TimeSpan ToTimeSpan(Byte[] value) { Int64 period = BitConverter.ToInt64(value, 0); period /= -10000000; return TimeSpan.FromSeconds(period); } public static Byte[] ToByteArray(TimeSpan value) { Double period = value.TotalSeconds; period *= -10000000; return BitConverter.GetBytes((Int64)period); } }

Vadims Podans

Wow, thanks, I never thinked about BitConverter :)

Thomas

I'm currently struggeling with the same issue. The following code seems to do it so far: $ad = [adsisearcher]"" $ad.SearchRoot = "LDAP://emea.forest.local:/CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=forest,DC=local" $ad.Filter = "(&(ObjectClass=pKICertificateTemplate)(name=UserCert))" $Template = $ad.FindOne() $y = [System.BitConverter]::ToInt64($Template.properties.pkiexpirationperiod[0],0) $z=[math]::abs($y) (($z/(365*24*60*60))/10000000).ToString() +" years" $y = [System.BitConverter]::ToInt64($Template.properties.pkioverlapperiod[0],0) $z=[math]::abs($y) (($z/(7*24*60*60))/10000000).ToString() +" weeks"

Max

-- $CertTemplate='User' $CertTemplateValidDays=30 -- $ValidCertUnitDay=[int64](24*(60*(60))) # (24 hours * (60 minutes * (60 seconds))) == 1 day $ValidADUnits=[int64](-10000000) # -((10000000 * (100 nanoseconds)) == 1 second) negative filetime syntax $Cer

Chris Dent

Thanks for this one, neglected to account for the signing bit in my conversion attempt :) To add a little, you can create a new TimeSpan object using the converted ticks (squashed onto one line): New-Object TimeSpan([Math]::Abs([BitConverter]::ToInt64($ByteArray, 0))) This may help simplify some of the time span conversion techniques above (Seconds, Minutes, Hours and Days are presented for you). Chris

Ryan Voice

Glad I found this, put me on an interesting path. This is the solution I've come up with to make my life "easy"

$Start = [datetime]::FromFileTime("0")
$End = [datetime]::FromFileTime([System.BitConverter]::ToInt64($Object.pKIExpirationPeriod, 0) * -1)
New-TimeSpan -Start $Start -End $End


Post your comment:

Please, solve this little equation and enter result below. Captcha