如何在PowerShell中正确将FILETIME类型转换为DATETIME
问题背景
- 参考此前发布的相关主题
- 正在开发的解决方案需要在Windows Server环境中通过PowerShell运行,底层依托.NET Framework实现。
- 由于微软PKI模块未提供对应功能的专用cmdlet,查找了可实现相关操作的Windows原生函数,最终在wincrypt.h、timezoneapi.h、minwinbase.h头文件中找到了目标接口。
- 微软官方文档中的代码示例均为C++编写,需要将Managed Types及其他数据类型转换为C#定义,才能将相关函数与结构体集成到PowerShell脚本中。
- 目前所有操作FILETIME类型属性或参数的尝试均失败,试了多种时间纪元起点计算方式,得到的结果始终不符合预期,测试用的纪元定义代码如下:
$PowerShell = [DateTime]::new(0001,1,1,0,0,0,[System.DateTimeKind]::Utc) $FileTime = [DateTime]::new(1601,1,1,0,0,0,[System.DateTimeKind]::Utc) $UnixEpoch = [DateTime]::new(1970,1,1,0,0,0,[System.DateTimeKind]::Utc)
- 现需要正确实现FILETIME到DATETIME的转换方法,当前编写的测试脚本如下:
$Cryptnet = @' [DllImport("Cryptnet.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool CryptRetrieveObjectByUrl ( string pszUrl, uint pszObjectOid, uint dwRetrievalFlags, uint dwTimeout, ref IntPtr ppvObject, IntPtr hAsyncRetrieve, IntPtr pCredentials, IntPtr pvVerify, IntPtr pAuxInfo ); '@ Microsoft.PowerShell.Utility\Add-Type -MemberDefinition $Cryptnet -Name Cryptnet -Namespace Win32 $Crypt32 = @' [DllImport("Crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool CertFreeCRLContext ( IntPtr pCrlContext ); [StructLayout(LayoutKind.Sequential)] public struct CRL_CONTEXT { public uint dwCertEncodingType; public IntPtr pbCrlEncoded; public uint cbCrlEncoded; public IntPtr pCrlInfo; public IntPtr hCertStore; }; [StructLayout(LayoutKind.Sequential)] public struct CRL_INFO { public uint dwVersion; public IntPtr SignatureAlgorithm; public IntPtr Issuer; public System.Runtime.InteropServices.ComTypes.FILETIME ThisUpdate; public System.Runtime.InteropServices.ComTypes.FILETIME NextUpdate; public uint cCRLEntry; public IntPtr rgCRLEntry; public uint cExtension; public IntPtr rgExtension; }; '@ Microsoft.PowerShell.Utility\Add-Type -MemberDefinition $Crypt32 -Name Crypt32 -Namespace Win32 <# # 如果CRL_INFO结构体的ThisUpdate和NextUpdate属性声明为IntPtr类型,需要用FILETIME结构体将指针转换为底层结构。 $Kernel32 = @' [StructLayout(LayoutKind.Sequential, Pack = 4)] public struct FILETIME { public uint dwLowDateTime; public uint dwHighDateTime; }; '@ Microsoft.PowerShell.Utility\Add-Type -MemberDefinition $Kernel32 -Name Kernel32 -Namespace Win32 #> $pszUrl = 'http://pki.contoso.com/cadp/Contoso-RootCA.crl' # 待检索的PKI对象地址 $pszObjectOid = 2 # CONTEXT_OID_CRL $dwRetrievalFlags = 4 # CRYPT_WIRE_ONLY_RETRIEVAL $dwTimeout = 0 # 设为0时函数不会超时 $ppvObject = [System.IntPtr]::Zero # 返回对象的指针地址 $hAsyncRetrieve = [System.IntPtr]::Zero # 保留参数,必须设为NULL $pCredentials = [System.IntPtr]::Zero # 未使用参数 $pvVerify = [System.IntPtr]::Zero # 设为NULL表示调用方不需要获取签名者的证书上下文或索引 $pAuxInfo = [System.IntPtr]::Zero # 非NULL且设置了结构体cbSize成员时,返回上次成功有线检索的时间 $CryptRetrieveObjectByUrl = [Win32.Cryptnet]::CryptRetrieveObjectByUrl( $pszUrl, $pszObjectOid, $dwRetrievalFlags, $dwTimeout, [ref]$ppvObject, $hAsyncRetrieve, $pCredentials, $pvVerify, $pAuxInfo ) if ($CryptRetrieveObjectByUrl) { $CRL_CONTEXT = [System.Runtime.InteropServices.Marshal]::PtrToStructure($ppvObject,[System.Type][Win32.Crypt32+CRL_CONTEXT]) [System.Void][Win32.Crypt32]::CertFreeCRLContext($ppvObject) $CRL_INFO = [System.Runtime.InteropServices.Marshal]::PtrToStructure($CRL_CONTEXT.pCrlInfo,[System.Type][Win32.Crypt32+CRL_INFO]) $NextUpdate = [System.Int64]($CRL_INFO.NextUpdate.dwHighDateTime -shl 32) -bor [System.Int32]($CRL_INFO.NextUpdate.dwLowDateTime) [System.DateTime]::FromFileTimeUtc($NextUpdate) $ThisUpdate = ($CRL_INFO.ThisUpdate.dwHighDateTime -shl 32) -bor ($CRL_INFO.ThisUpdate.dwLowDateTime) [System.DateTime]::FromFileTimeUtc($ThisUpdate) } else { $LastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() $Exception = Microsoft.PowerShell.Utility\New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $LastWin32Error $ErrorId = 'CryptRetrieveObjectByUrl' $ErrorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified $TargetObject= $null $ErrorRecord = Microsoft.PowerShell.Utility\New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $Exception, $ErrorId, $ErrorCategory, $TargetObject Write-Error -ErrorRecord $ErrorRecord }
解决方案
你的代码有两处核心错误,直接导致时间转换结果异常:
- 内存释放时机错误:在读取
CRL_CONTEXT.pCrlInfo指向的结构体内容之前,就调用CertFreeCRLContext释放了上下文,对应内存会被系统回收,后续Marshal读取到的是无效内存里的乱码数据,时间值自然不符合预期。必须先读完CRL_INFO结构体,再释放CRL上下文。 - 位运算类型转换错误:
dwLowDateTime是32位无符号整数,直接将其转为Int32时,若值超过2^31-1会触发符号位扩展,拼接出来的64位FILETIME值完全错误。拼接时需要先将dwLowDateTime转为UInt32,再参与位运算。
不需要自己额外定义FILETIME结构体,你当前引用的System.Runtime.InteropServices.ComTypes.FILETIME已经是系统内置的正确定义,可直接使用;也不需要手动做纪元加减,DateTime.FromFileTimeUtc本身已经内置了1601年纪元的转换逻辑。
修正后的核心执行代码如下:
if ($CryptRetrieveObjectByUrl) { $CRL_CONTEXT = [System.Runtime.InteropServices.Marshal]::PtrToStructure($ppvObject,[System.Type][Win32.Crypt32+CRL_CONTEXT]) # 先读取结构体内容,再释放上下文 $CRL_INFO = [System.Runtime.InteropServices.Marshal]::PtrToStructure($CRL_CONTEXT.pCrlInfo,[System.Type][Win32.Crypt32+CRL_INFO]) [System.Void][Win32.Crypt32]::CertFreeCRLContext($ppvObject) # 按无符号值拼接高低位,避免符号位错误 $NextUpdate = [DateTime]::FromFileTimeUtc(([Int64]$CRL_INFO.NextUpdate.dwHighDateTime -shl 32) -bor [UInt32]$CRL_INFO.NextUpdate.dwLowDateTime) $ThisUpdate = [DateTime]::FromFileTimeUtc(([Int64]$CRL_INFO.ThisUpdate.dwHighDateTime -shl 32) -bor [UInt32]$CRL_INFO.ThisUpdate.dwLowDateTime) # 输出结果 $ThisUpdate $NextUpdate }
内容的提问来源于stack exchange,提问作者Kanuc
相关产品推荐
相关产品推荐

