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:
When you look in UI, you will see that certificates issued based on Web Server template are valid for 2 years:
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!
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); } }
Wow, thanks, I never thinked about BitConverter :)
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"
-- $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
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
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:
Comments: