I've got some working C# code to work with some Windows API calls (based on my Get-CertificatePath.ps1 PowerShell script), but the F# version fails. I'm sure I'm missing something, most likely an attribute, but I haven't been able to figure it out.
Is there a way to get the F# version working?
Run using csi.exe
from the microsoft-build-tools
Chocolatey package.
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));
The output is:
found 1 certs
private key? True
key name: 32fd60███████████████████████████████████-████-████-████-████████████
Run using dotnet fsi
(version 3.1.100; Chocolatey package dotnetcore
as of this posting).
#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"
The output is:
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
This is required for the .fsx file above to work, then run 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
Edit: Initializing let mutable privateKey = new SafeNCryptKeyHandle ()
seems to fix the issue.
It looks like you have a parameter being passed a null that cannot be a null. The one that jumps out at me is this.
let mutable privateKey : SafeNCryptKeyHandle = null
Should this instead be a new object?
It looks like you need to declare the reference type of the function parameter corresponding to this document.
Here is also a sample from the answer of @phoog.
Using byref
in F#/ref
in C# requires initialization, try to use
let mutable privateKey : SafeNCryptKeyHandle = new SafeNCryptKeyHandle()
UPDATE:
The whole sample:
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"
Even though OP has a work-around I was wondering why the F# code crashed. After some digging around using dnspy
I found that the interop functions differed subtly.
First there is a bug in the F# interop code in that it should use winapi
calling conventon not cdecl
. However, fixing that bug didn't fix the issue.
What seems to make a difference is that the C# interop functions out parameters are tagged as 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
In F# they are 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
If I change the F# code to call the C# interop functions it works for me. This implies to me that the jitter adds verification to make sure that we don't pass null SafeHandle
if the it's a ref
parameter but not if it's out
.
I tried to add [Out]
attribute to the F# interop functions but they seem discarded (also note F# discarded [<MarshalAs(UnmanagedType.Bool)>]
). Perhaps there's some way to do it but the F# documentation is rather succinct in this regard.
Using outref<_>
:s didn't compile.
I also checked the F# parser code to see if I could see some way to add the out attribute but alas no I couldn't see a way to do it: https://github.com/dotnet/fsharp/blob/master/src/fsharp/pars.fsy#L2558
I did however understand why outref<_>
doesn't work as the cType
parser only supports very limited types.
I think creating an F# issue should be a reasonable next step. Hopefully it's just a documentation issue.
User contributions licensed under CC BY-SA 3.0