【问题标题】:Having trouble converting external Windows Crypto API calls from C# to F#将外部 Windows Crypto API 调用从 C# 转换为 F# 时遇到问题
【发布时间】:2019-12-30 20:59:01
【问题描述】:

我有一些工作 C# 代码可以处理一些 Windows API 调用(基于 my Get-CertificatePath.ps1 PowerShell script),但 F# 版本失败。我确定我遗漏了一些东西,很可能是一个属性,但我无法弄清楚。

有没有办法让 F# 版本正常工作?

keyname.csx

使用microsoft-build-tools Chocolatey 包中的csi.exe 运行。

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Permissions;
using Microsoft.Win32.SafeHandles;

public static class CryptoApi
{
    [DllImport("crypt32.dll")]
    internal static extern SafeNCryptKeyHandle CertDuplicateCertificateContext(IntPtr certContext); // CERT_CONTEXT *
    [DllImport("crypt32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool
        CryptAcquireCertificatePrivateKey(SafeNCryptKeyHandle pCert,
                                          uint dwFlags,
                                          IntPtr pvReserved, // void *
                                          [Out] out SafeNCryptKeyHandle phCryptProvOrNCryptKey,
                                          [Out] out int dwKeySpec,
                                          [Out, MarshalAs(UnmanagedType.Bool)] out bool pfCallerFreeProvOrNCryptKey);
    [SecurityCritical]
    [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
    public static string GetCngUniqueKeyContainerName(X509Certificate2 certificate)
    {
        SafeNCryptKeyHandle privateKey = null;
        int keySpec = 0;
        bool freeKey = true;
        CryptAcquireCertificatePrivateKey(CertDuplicateCertificateContext(certificate.Handle),
                                          0x00040000, // AcquireOnlyNCryptKeys
                                          IntPtr.Zero, out privateKey, out keySpec, out freeKey);
        return CngKey.Open(privateKey, CngKeyHandleOpenOptions.None).UniqueName;
    }
}

X509Certificate2 getMyCert(string subject)
{
    using(var store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
    {
        store.Open(OpenFlags.OpenExistingOnly);
        var certs = store.Certificates.Find(X509FindType.FindBySubjectName, subject, false);
        Console.WriteLine("found {0} certs", certs.Count);
        store.Close();
        return certs[0];
    }
}

var cert = getMyCert("localhost");
Console.WriteLine("private key? {0}", cert.HasPrivateKey);
Console.WriteLine("key name: {0}", CryptoApi.GetCngUniqueKeyContainerName(cert));

输出是:

found 1 certs
private key? True
key name: 32fd60███████████████████████████████████-████-████-████-████████████

keyname.fsx

使用 dotnet fsi 运行(版本 3.1.100;Chocolatey 包 dotnetcore 在此发布时)。

#load ".paket/load/netstandard2.1/System.Security.Cryptography.Cng.fsx"
#load ".paket/load/netstandard2.1/System.Security.Cryptography.X509Certificates.fsx"

open System
open System.Diagnostics.CodeAnalysis
open System.Runtime.ConstrainedExecution
open System.Runtime.InteropServices
open System.Security
open System.Security.Cryptography
open System.Security.Cryptography.X509Certificates
open System.Security.Permissions
open Microsoft.Win32.SafeHandles

type CryptoApi () =
    [<DllImport("crypt32.dll", CallingConvention = CallingConvention.Cdecl)>]
    static extern SafeNCryptKeyHandle  internal CertDuplicateCertificateContext(IntPtr certContext) // CERT_CONTEXT *
    [<DllImport("crypt32.dll", CallingConvention = CallingConvention.Cdecl, SetLastError = true)>]
    static extern [<MarshalAs(UnmanagedType.Bool)>] bool internal
        CryptAcquireCertificatePrivateKey(SafeNCryptKeyHandle  pCert,
                                          uint32 dwFlags,
                                          IntPtr pvReserved, // void *
                                          SafeNCryptKeyHandle& phCryptProvOrNCryptKey,
                                          int& dwKeySpec,
                                          [<MarshalAs(UnmanagedType.Bool)>] bool& pfCallerFreeProvOrNCryptKey)
    [<SecurityCritical>]
    [<SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)>]
    static member GetCngUniqueKeyContainerName (certificate : X509Certificate2) =
        let mutable privateKey : SafeNCryptKeyHandle = null
        let mutable keySpec = 0
        let mutable freeKey = true
        CryptAcquireCertificatePrivateKey(CertDuplicateCertificateContext(certificate.Handle),
                                          0x00040000u, // AcquireOnlyNCryptKeys
                                          IntPtr.Zero, &privateKey, &keySpec, &freeKey) |> ignore
        CngKey.Open(privateKey, CngKeyHandleOpenOptions.None).UniqueName

let getMyCert subject =
    use store = new X509Store(StoreName.My, StoreLocation.CurrentUser)
    store.Open(OpenFlags.OpenExistingOnly)
    let certs = store.Certificates.Find(X509FindType.FindBySubjectName, subject, false)
    printfn "found %d certs" certs.Count
    store.Close()
    certs.[0]

let cert = getMyCert "localhost"
printfn "private key? %b" cert.HasPrivateKey
CryptoApi.GetCngUniqueKeyContainerName(cert) |> printfn "%s"

输出是:

found 1 certs
private key? true
System.ArgumentNullException: SafeHandle cannot be null. (Parameter 'pHandle')
   at System.StubHelpers.StubHelpers.SafeHandleAddRef(SafeHandle pHandle, Boolean& success)
   at System.StubHelpers.StubHelpers.AddToCleanupList(CleanupWorkListElement& pCleanupWorkList, SafeHandle handle)
   at FSI_0003.CryptoApi.CryptAcquireCertificatePrivateKey(SafeNCryptKeyHandle pCert, UInt32 dwFlags, IntPtr pvReserved, SafeNCryptKeyHandle& phCryptProvOrNCryptKey, Int32& dwKeySpec, Boolean& pfCallerFreeProvOrNCryptKey)
   at FSI_0003.CryptoApi.GetCngUniqueKeyContainerName(X509Certificate2 certificate)
   at <StartupCode$FSI_0003>.$FSI_0003.main@()
Stopped due to error

paket.dependencies

这是上述 .fsx 文件运行所必需的,然后运行 ​​paket install

generate_load_scripts: true
source https://api.nuget.org/v3/index.json

storage: none
framework: netcore3.0, netstandard2.0, netstandard2.1
nuget System.Security.Cryptography.Cng
nuget System.Security.Cryptography.X509Certificates

编辑:初始化let mutable privateKey = new SafeNCryptKeyHandle () 似乎可以解决问题。

【问题讨论】:

标签: winapi f# cryptoapi


【解决方案1】:

看起来你有一个参数被传递了一个不能为空的空值。让我眼前一亮的是这个。

let mutable privateKey : SafeNCryptKeyHandle = null

这应该是一个新对象吗?

【讨论】:

  • 该空值在 C# 版本中工作正常。
  • 我想我至少应该尝试一下,即使它看起来没有意义。这就是答案。
  • F# 对空值的平均要求更加严格。
  • 我想我认为填充值的行为是在 crypt32.dll 中实现的,但显然 F# 必须以不同的方式编组空值,然后才能走得那么远。
【解决方案2】:

看来需要声明this document对应的函数参数的引用类型。

这也是@phoog 的回答中的sample

在F#中使用byref/在C#中使用ref需要初始化,尝试使用

let mutable privateKey : SafeNCryptKeyHandle = new SafeNCryptKeyHandle()

更新:

整个样本:

open System
open System.Diagnostics.CodeAnalysis
open System.Runtime.ConstrainedExecution
open System.Runtime.InteropServices
open System.Security
open System.Security.Cryptography
open System.Security.Cryptography.X509Certificates
open System.Security.Permissions
open Microsoft.Win32.SafeHandles

type CryptoApi () =
    [<DllImport("crypt32.dll", CallingConvention = CallingConvention.Cdecl)>]
    static extern SafeNCryptKeyHandle  internal CertDuplicateCertificateContext(IntPtr certContext) // CERT_CONTEXT *
    [<DllImport("crypt32.dll", CallingConvention = CallingConvention.Cdecl, SetLastError = true)>]
    static extern [<MarshalAs(UnmanagedType.Bool)>] bool internal
        CryptAcquireCertificatePrivateKey(SafeNCryptKeyHandle  pCert,
                                          uint32 dwFlags,
                                          IntPtr pvReserved, // void *
                                          SafeNCryptKeyHandle& phCryptProvOrNCryptKey,
                                          int& dwKeySpec,
                                          [<MarshalAs(UnmanagedType.Bool)>] bool& pfCallerFreeProvOrNCryptKey)
    [<SecurityCritical>]
    [<SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)>]
    static member GetCngUniqueKeyContainerName (certificate : X509Certificate2) =
        let mutable privateKey : SafeNCryptKeyHandle = new SafeNCryptKeyHandle()
        let mutable keySpec = 0
        let mutable freeKey = true
        CryptAcquireCertificatePrivateKey(CertDuplicateCertificateContext(certificate.Handle),
                                          0x00040000u, // AcquireOnlyNCryptKeys
                                          IntPtr.Zero, &privateKey, &keySpec, &freeKey) |> ignore
        CngKey.Open(privateKey, CngKeyHandleOpenOptions.None).UniqueName

let getMyCert subject =
    use store = new X509Store(StoreName.My, StoreLocation.CurrentUser)
    store.Open(OpenFlags.OpenExistingOnly)
    let certs = store.Certificates.Find(X509FindType.FindBySubjectName, subject, false)
    printfn "found %d certs" certs.Count
    store.Close()
    certs.[0]

let cert = getMyCert "localhost"
printfn "private key? %b" cert.HasPrivateKey
CryptoApi.GetCngUniqueKeyContainerName(cert) |> printfn "%s"

【讨论】:

  • 参数似乎同时标记为InOut refs(尽管C# 版本似乎不需要传入任何有效值)。 pinvoke.net/default.aspx/… 但是,WinAPI 函数是 C++ 互操作(P/Invoke),而不是 C# 互操作,所以我不确定这种语法是否有效。
  • 当我尝试使用参数语法 outref&lt;SafeNCryptKeyHandle&gt; phCryptProvOrNCryptKeySafeNCryptKeyHandle outref phCryptProvOrNCryptKey 时,它无法编译。此语法似乎与 extern 声明不兼容。
  • 您在 F# 中的问题与在 C# 中将 out 替换为 ref 相同。
  • 有趣,那么让我尝试将&amp; 替换为[&lt;Out&gt;]。这是我开始的地方,但我可能已经改变了其他事情。
  • 对不起,我没有说清楚。我的意思是,如果你使用&amp;(这代表byref类型),你需要new safencryptkeyhandle(),要恢复你原来的代码,只需修改let mutable privatekey: safencryptkeyhandle = new safencryptkeyhandle()
【解决方案3】:

尽管 OP 有解决方法,但我想知道为什么 F# 代码会崩溃。在使用dnspy 进行一些挖掘之后,我发现互操作函数略有不同。

首先,F# 互操作代码中存在一个错误,即它应该使用 winapi 调用 conventon 而不是 cdecl。但是,修复该错误并不能解决问题。

似乎有所不同的是,C# 互操作函数输出参数被标记为out

.method assembly hidebysig static pinvokeimpl("crypt32.dll" lasterr winapi) 
  bool marshal(bool) CryptAcquireCertificatePrivateKey (
    class [System.Security.Cryptography.Cng]Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle pCert,
    uint32 dwFlags,
    native int pvReserved,
    [out] class [System.Security.Cryptography.Cng]Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle& phCryptProvOrNCryptKey,
    [out] int32& dwKeySpec,
    [out] bool& marshal(bool) pfCallerFreeProvOrNCryptKey
  ) cil managed preservesig 

在 F# 中它们是 ref:

.method assembly static pinvokeimpl("crypt32.dll" lasterr cdecl) 
  bool CryptAcquireCertificatePrivateKey (
    class [System.Security.Cryptography.Cng]Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle pCert,
    uint32 dwFlags,
    native int pvReserved,
    class [System.Security.Cryptography.Cng]Microsoft.Win32.SafeHandles.SafeNCryptKeyHandle& phCryptProvOrNCryptKey,
    int32& dwKeySpec,
    bool& pfCallerFreeProvOrNCryptKey
  ) cil managed preservesig 

如果我更改 F# 代码以调用 C# 互操作函数,它对我有用。这对我来说意味着抖动添加了验证,以确保我们不会传递 null SafeHandle,如果它是 ref 参数,但不是 out

我尝试将 [Out] 属性添加到 F# 互操作函数,但它们似乎已被丢弃(另请注意 F# 已丢弃 [&lt;MarshalAs(UnmanagedType.Bool)&gt;])。也许有一些方法可以做到这一点,但F# documentation 在这方面相当简洁。

使用outref&lt;_&gt;:s 无法编译。

我还检查了 F# 解析器代码,看看是否可以找到添加 out 属性的方法,但遗憾的是,我看不到添加属性的方法:https://github.com/dotnet/fsharp/blob/master/src/fsharp/pars.fsy#L2558

不过,我确实理解为什么 outref&lt;_&gt; 不起作用,因为 cType 解析器只支持非常有限的类型。

我认为创建 F# 问题应该是合理的下一步。希望这只是一个文档问题。

【讨论】:

  • 感谢您为此深入挖掘!如果这是您想要报告的内容,我不确定我是否能够在如此低的级别上阐明或捍卫错误报告。
猜你喜欢
  • 2019-12-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-11
  • 2014-02-28
  • 2022-01-25
相关资源
最近更新 更多