将外部 Windows 加密 API 调用从 C# 转换为 F# 时遇到问题
Having trouble converting external Windows Crypto API calls from C# to F#
我有一些工作的 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 ()
似乎解决了这个问题。
您似乎有一个参数被传递给一个不能为 null 的 null。跳到我面前的是这个。
let mutable privateKey : SafeNCryptKeyHandle = null
这应该是一个新对象吗?
貌似需要声明this document对应的函数参数的引用类型。
这里还有一个来自@phoog 的回答。
在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"
尽管 OP 有解决方法,但我想知道为什么 F# 代码会崩溃。在使用 dnspy
进行一些挖掘之后,我发现互操作函数有细微的差别。
首先,F# 互操作代码中存在一个错误,它应该使用 winapi
调用约定而不是 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# 已丢弃 [<MarshalAs(UnmanagedType.Bool)>]
)。也许有一些方法可以做到这一点,但 F# documentation 在这方面相当简洁。
使用 outref<_>
:s 没有编译。
我还检查了 F# 解析器代码,看看是否可以找到添加 out 属性的方法,但遗憾的是,我看不到这样做的方法:https://github.com/dotnet/fsharp/blob/master/src/fsharp/pars.fsy#L2558
不过我明白为什么 outref<_>
不起作用,因为 cType
解析器只支持非常有限的类型。
我认为创建 F# 问题应该是合理的下一步。希望这只是文档问题。
我有一些工作的 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 ()
似乎解决了这个问题。
您似乎有一个参数被传递给一个不能为 null 的 null。跳到我面前的是这个。
let mutable privateKey : SafeNCryptKeyHandle = null
这应该是一个新对象吗?
貌似需要声明this document对应的函数参数的引用类型。
这里还有一个来自@phoog 的回答
在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"
尽管 OP 有解决方法,但我想知道为什么 F# 代码会崩溃。在使用 dnspy
进行一些挖掘之后,我发现互操作函数有细微的差别。
首先,F# 互操作代码中存在一个错误,它应该使用 winapi
调用约定而不是 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# 已丢弃 [<MarshalAs(UnmanagedType.Bool)>]
)。也许有一些方法可以做到这一点,但 F# documentation 在这方面相当简洁。
使用 outref<_>
:s 没有编译。
我还检查了 F# 解析器代码,看看是否可以找到添加 out 属性的方法,但遗憾的是,我看不到这样做的方法:https://github.com/dotnet/fsharp/blob/master/src/fsharp/pars.fsy#L2558
不过我明白为什么 outref<_>
不起作用,因为 cType
解析器只支持非常有限的类型。
我认为创建 F# 问题应该是合理的下一步。希望这只是文档问题。