Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calling C# SDK from Python results in "Unable to load DLL 'Yubico.NativeShims'" error #47

Closed
johanrex opened this issue Apr 18, 2023 · 16 comments

Comments

@johanrex
Copy link

My team uses python for analytics purposes. We have a need to encrypt large datasets and to protect the one-time random symmetric encryption keys with a certificate on a YubiKey. We also have the requirement to do memory wipe of the memory that stores the symmetric encryption key. All the normal encryption packages for python store their encryption keys in a “bytes” object, and makes lots of internal copies of the encryption key. A bytes object in python is defined as immutable. I.e. it’s impossible to do memory wipe in python. My goal is to handle all encryption, certificate, and memory wipe related things with your C# SDK and package it in a dll that is called from python via the pythonnet package.

Unfortunately even the simplest thing results in this exception:

Exception has occurred: DllNotFoundException
Unable to load DLL 'Yubico.NativeShims' or one of its dependencies: The specified module could not be found. (0x8007007E)
at Yubico.PlatformInterop.NativeMethods.SCardEstablishContext(SCARD_SCOPE scope, SCardContext& context)
at Yubico.Core.Devices.SmartCard.DesktopSmartCardDeviceListener..ctor()
at Yubico.Core.Devices.SmartCard.SmartCardDeviceListener.Create()
at Yubico.YubiKey.YubiKeyDeviceListener..ctor()
at Yubico.YubiKey.YubiKeyDeviceListener.<>c.<.cctor>b__36_0()
at System.Lazy1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
at System.Lazy`1.CreateValue()
at Yubico.YubiKey.YubiKeyDeviceListener.get_Instance()
at Yubico.YubiKey.YubiKeyDevice.FindByTransport(Transport transport)
at Yubico.YubiKey.YubiKeyDevice.FindAll()
at testlib.Class1.CountDevices() in C:\git\pythonnet_test\cs\testlib\Class1.cs:line 9
at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)

I have prepared a sample project to replicate the issue here:
https://github.com/johanrex/pythonnet_test

In order to replicate it:

  • Clone the repo.
  • Direct your dotnet 7 capable shell to the testlib cs folder, e.g. c:\git\pythonnet_test\cs\testlib
  • Publish the project with “dotnet publish -o out
  • Direct your python capable shell to the py folder, e.g. C:\git\pythonnet_test\py
  • You might need to pip install pythonnet if it’s not already installed in your env.
  • You might need to update the hardcoded paths in the .py file.
  • Run the pythonnet_test.py file.

This is the same problem that David Coons have reported here:
microsoft/vstest#4343

I have created a ticket for it (converted from issue to discussion by the pythonnet developer) here, but realistically I’m not getting any further on that:
pythonnet/pythonnet#2124

I have written to Yubico support (ticket 420093). Unfortunately, the proposed solution there can’t be applied to my contrived scenario with python->C# interop.

I do have odd requirements, which results in this contrived solution. One can have opinions on how reasonable the memory wipe requirement is, but the requirement will not go away. I really hope it's possible to find a way to use the C# SDK from Python. Seeing as you have the same issue with vstest perhaps there is something weird going on when publishing.

@johanrex
Copy link
Author

Ping @coonsd.

@GregDomzalski
Copy link
Contributor

I'll take a look at the sample project. But this is outside of our usual supported scenarios.

I do want to make it very clear though - .NET is a managed memory runtime, just like Python. While we do have some better facilities for managing sensitive memory in .NET, we too are not able to clear every instance in every case. There are cases where scrubbing the memory completely is essentially impossible - and we made the call at the time to clear memory in a best effort fashion.

To be more specific: The .NET memory allocator and garbage collector can relocate memory at any time. That means, even if we clear the memory using the variable (handle) that we have, the data may still exist in an older and now inaccessible region of memory. Given some signals we saw from the .NET project as well as some internal security audits, we arrived at the conclusion that while non-zero, the risk was so small, and the attack vector so difficult, that we accepted this risk.

We talk a little bit about that in this document: https://docs.yubico.com/yesdk/users-manual/sdk-programming-guide/sensitive-data.html

Essentially, we consider that if someone has the ability to dump your process RAM and access it - there's really not much we can do.

At some point - I did see some chatter within the .NET project to add a runtime flag or something that would zero out old memory locations when a relocation occurs. You may want to investigate whether or not that has been implemented on their side and how to activate it.

If you need full memory protection, have you considered using the C library libykpiv instead? That would not be subject to the relocation issues, and I'm quite certain that we zero out sensitive data after use.

@johanrex
Copy link
Author

Thanks for looking into this @GregDomzalski.

I certainly understand there are limits to what you can support. I agree with everything you say.

At the same time, I have the dilemma that the requirement for memory wipe is something I need to fulfill. What I’m doing now is investigating every technical possibility to fulfill it.

Regarding libykpiv. I believe you haven’t published that as a separate project, right? At least I don’t find it as a separate repo. Only as part of the yubico-piv-tool repo. I did start to hack away at it but it involves calling some undocumented functions and basically reverse engineering yubico-piv-tool. It’s one path forward but it’s a time consuming one. Also, the C language is responsible for almost every security vulnerability ever so one must be careful. By trying to increase security I may end up in a situation where I make it worse. I believe using C# and accepting the best effort nature of the current limitations would be preferable.

The error I describe in this issue is interesting in that it's the same that David reported on the vstest project. It leads me to believe it might have something to do with how the SDK is published. If that means there is a configuration issue or actually a bug in dotnet publish is difficult to tell. So I really appreciate you taking the time to look into it.

@GregDomzalski
Copy link
Contributor

Took a look at your repro setup. Thank you very much for providing such a stripped down repro!

I believe this is a separate bug than what we reported on VSTest. Same symptom but different underlying causes.

In your case, you can fix this by copying the correct version of the Yubico.NativeShims.dll file into the path where you're running python from. So for example, I ran python3 ./pythonnet_test.py in your /py directory. I copied the dylib there (I'm on macOS) and was able to have the program complete successfully.

Unfortunately .NET has very poor guidance on how library authors should create, package, and integrate native components. The setup we have tends to work well enough within the .NET ecosystem (except for the VSTest bug we opened). It gets tricky once you exit the dotnet / MSBuild environment.

I don't think there's any action that I can take here other than advising you of this workaround. Your python build process would need to be enlightened about this DLL, or Pythonnet would need to somehow understand and interpret the build and buildTransitive elements of a NuGet package.

Sorry I don't really have a better answer that this. Hopefully this workaround is workable for your needs.

@johanrex
Copy link
Author

johanrex commented May 2, 2023

@GregDomzalski For some reason that doesn't work on Windows. I get the same error even when I copy the file to the same folder as the python script.


pythonnet_test\py on  main [?] via 🐍 v3.9.16 via 🅒 pythonnet
❯ pwd

Path
----
C:\git\pythonnet_test\py


pythonnet_test\py on  main [?] via 🐍 v3.9.16 via 🅒 pythonnet
❯ cp C:\git\pythonnet_test\cs\testlib\out\runtimes\win-x64\native\Yubico.NativeShims.dll .

pythonnet_test\py on  main [?] via 🐍 v3.9.16 via 🅒 pythonnet
❯ dir

    Directory: C:\git\pythonnet_test\py

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---          2023-04-18    09:36            514 pythonnet_test.py
-a---          2023-02-03    01:39        2860632 Yubico.NativeShims.dll


pythonnet_test\py on  main [?] via 🐍 v3.9.16 via 🅒 pythonnet
❯ python .\pythonnet_test.py
Traceback (most recent call last):
  File "C:\git\pythonnet_test\py\pythonnet_test.py", line 23, in <module>
    small_test()
  File "C:\git\pythonnet_test\py\pythonnet_test.py", line 17, in small_test
    n = Class1().CountDevices()
System.DllNotFoundException: Unable to load DLL 'Yubico.NativeShims' or one of its dependencies: The specified module could not be found. (0x8007007E)
   at Yubico.PlatformInterop.NativeMethods.SCardEstablishContext(SCARD_SCOPE scope, SCardContext& context)
   at Yubico.Core.Devices.SmartCard.DesktopSmartCardDeviceListener..ctor()
   at Yubico.Core.Devices.SmartCard.SmartCardDeviceListener.Create()
   at Yubico.YubiKey.YubiKeyDeviceListener..ctor()
   at Yubico.YubiKey.YubiKeyDeviceListener.<>c.<.cctor>b__36_0()
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at Yubico.YubiKey.YubiKeyDeviceListener.get_Instance()
   at Yubico.YubiKey.YubiKeyDevice.FindByTransport(Transport transport)
   at Yubico.YubiKey.YubiKeyDevice.FindAll()
   at testlib.Class1.CountDevices() in C:\git\pythonnet_test\cs\testlib\Class1.cs:line 9
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

pythonnet_test\py on  main [?] via 🐍 v3.9.16 via 🅒 pythonnet
❯

Here's my dotnet details:

❯ dotnet --info
.NET SDK:
 Version:   7.0.203
 Commit:    5b005c19f5

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19044
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\7.0.203\

Host:
  Version:      7.0.5
  Architecture: x64
  Commit:       8042d61b17

.NET SDKs installed:
  7.0.203 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.16 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.16 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 6.0.16 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 7.0.5 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found:
  x86   [C:\Program Files (x86)\dotnet]
    registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables:
  Not set

global.json file:
  Not found

Learn more:
  https://aka.ms/dotnet/info

Download .NET:
  https://aka.ms/dotnet/download

All of this is running inside a powershell shell with this info:

❯ $psversiontable

Name                           Value
----                           -----
PSVersion                      7.3.1
PSEdition                      Core
GitCommitId                    7.3.1
OS                             Microsoft Windows 10.0.19044
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Using this conda environment:

 conda info

     active environment : pythonnet
    active env location : C:\Users\serexj\Miniconda3\envs\pythonnet
            shell level : 2
       user config file : C:\Users\serexj\.condarc
 populated config files : C:\Users\serexj\.condarc
          conda version : 23.3.1
    conda-build version : not installed
         python version : 3.10.11.final.0
       virtual packages : __archspec=1=x86_64
                          __cuda=11.4=0
                          __win=0=0
       base environment : C:\Users\serexj\Miniconda3  (writable)
      conda av data dir : C:\Users\serexj\Miniconda3\etc\conda
  conda av metadata url : None
           channel URLs : https://repo.anaconda.com/pkgs/main/win-64
                          https://repo.anaconda.com/pkgs/main/noarch
                          https://repo.anaconda.com/pkgs/r/win-64
                          https://repo.anaconda.com/pkgs/r/noarch
                          https://repo.anaconda.com/pkgs/msys2/win-64
                          https://repo.anaconda.com/pkgs/msys2/noarch
          package cache : C:\Users\serexj\Miniconda3\pkgs
                          C:\Users\serexj\.conda\pkgs
                          C:\Users\serexj\AppData\Local\conda\conda\pkgs
       envs directories : C:\Users\serexj\Miniconda3\envs
                          C:\Users\serexj\.conda\envs
                          C:\Users\serexj\AppData\Local\conda\conda\envs
               platform : win-64
             user-agent : conda/23.3.1 requests/2.28.1 CPython/3.10.11 Windows/10 Windows/10.0.19044
          administrator : False
             netrc file : None
           offline mode : False

@GregDomzalski
Copy link
Contributor

I would expect that you need to put the DLL in the same folder as the executable that is being run. This might be Python. There's also the heavy hammer of putting the DLL in C:\Windows\System32. Not that I really recommend doing that...

What you could do that may help inform a better decision is to use ProcMon to see what process is trying to load NativeShims.dll and where it is looking for it. https://learn.microsoft.com/en-us/sysinternals/downloads/procmon

You can set up a filter on the path and look for things that contain "Yubico.NativeShims.dll". I would expect that when you run your program, we would get a bunch of "FILE_NOT_FOUND" entries and various paths that contain the DLL that don't actually exist. Typically the first path that it tries will be the one that we'd like to use.

@virot
Copy link

virot commented May 9, 2023

Funny thing. The only places it seems to read is:

  • C:\Windows\System32\WindowsPowerShell\v1.0\Yubico.NativeShims
  • C:\Windows\System32\Yubico.NativeShims

So I copied it to C:\Windows\System32\WindowsPowerShell\v1.0\Yubico.NativeShims without .dll, otherwise it doesn't work. And now I am getting a issue with system.memory. But this is progress. The question is why is it only loading those two locations and why no dll extension.

@GregDomzalski
Copy link
Contributor

So, there are multiple versions of our assemblies being packaged. There's one for net472 and one for netstandard20. If you are targeting the PowerShell that is built into Windows (which it looks like from those paths), you should be using the net472 version. That should have the correction to use the .dll extension and may also resolve some of the other System.Memory and other dependency issues.

@virot
Copy link

virot commented May 9, 2023

Switching to building for net472, fix that the Yubico.NativeShims didnt have a dll extention. But I still have the different versions of System.Memory. Looks like I need to figure out how to do bindingRedirect.
I am still getting error about 4.0.1.1 missing, that is 4.5.4 and System.Formats.Cbor required 4.5.5.

But this is really helpful. I would love not to need to put the DLL in the protected folders but that might be out of your hands.

@GregDomzalski
Copy link
Contributor

Sorry, I meant that you need the net472 version of Yubico.YubiKey, Yubico.Core, and Yubico.DotnetPolyfills. You can likely remain using the Yubico.NativeShims version that you have already been using.

@GregDomzalski
Copy link
Contributor

Yes, unfortunately where the DLL is loaded is up to Windows and not us 😄

@virot
Copy link

virot commented May 10, 2023

Sorry I didnt explain enough. The reason why it took so long for me to get back, is that I learned how to do a compiled powershell module. So I just set the target and then did a dotnet build and process takes the dlls from %HOMEPATH%.nuget\packages\yubico.yubikey\1.7.0 etc..

And since my code is now a compiled DLL i think I need to understand bindingredirects as yubico.yubikey and its dependency:

  • Yubico.Core: System.Memory >= 4.5.4
  • System.Formats.Cbor: System.Memory >= 4.5.5

@virot
Copy link

virot commented May 10, 2023

My GOD, I Am so sorry.. I have replied to the wrong issue, I was reading this and then replied in the wrong.. So sorry for all involved.

@GregDomzalski
Copy link
Contributor

I'll leave the comments above, as they may actually be relevant to this thread as well. Just replace PowerShell and Python and the same investigations would be the same 😃

@johanrex
Copy link
Author

@GregDomzalski Nice trick with procmon! Tried it and it gave these clues:
image

I copied the file to one of those locations and it worked!
image

Even though it's not great idea to copy dll files around like that it would work as a workaround.

We can see with procmon that it tries a bunch of paths. I'm not sure what is suggesting those. Do you know if this is a peculiarity of python or windows? And of course, do you know if there is a way to affect it somehow?

@johanrex
Copy link
Author

johanrex commented May 12, 2023

I found that you can use os.add_dll_directory to give the path to the native dll to temporarily give another path in the dll resolve chain. That's a much neater solution/workaround than copying files around. E.g.

    import os
    with os.add_dll_directory(r"C:\git\pythonnet_test\cs\testlib\out\runtimes\win-x64\native"):
        from pythonnet import load

        runtimeconfig_path = r"C:\git\pythonnet_test\cs\testlib\out\testlib.runtimeconfig.json"
        load("coreclr", runtime_config=runtimeconfig_path)

        import clr
        import sys

        sys.path.append(r"C:\git\pythonnet_test\cs\testlib\out")
        clr.AddReference("testlib")

        from testlib import Class1
        n = Class1().CountDevices()

        print(f"Found {n} devices")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants