Having trouble converting external Windows Crypto API calls from C# to F#

1

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?

keyname.csx

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███████████████████████████████████-████-████-████-████████████

keyname.fsx

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

paket.dependencies

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.

winapi
f#
cryptoapi
asked on Stack Overflow Dec 30, 2019 by brianary • edited Dec 31, 2019 by brianary

3 Answers

2

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?

answered on Stack Overflow Dec 30, 2019 by VoronoiPotato
2

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"
answered on Stack Overflow Dec 31, 2019 by Drake Wu • edited Dec 31, 2019 by Drake Wu
1

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