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

.net core TLS server certificate with private key in TPM #109243

Open
appcodr opened this issue Oct 25, 2024 · 21 comments
Open

.net core TLS server certificate with private key in TPM #109243

appcodr opened this issue Oct 25, 2024 · 21 comments
Assignees
Milestone

Comments

@appcodr
Copy link

appcodr commented Oct 25, 2024

I'm trying to do an equivalent of this command in a .net core library for doing a TLS server where the private key is in TPM with a reference of 0x8100001:

openssl s_server -cert rsa.crt -key 0x8100002-keyform engine -engine tpm2tss -accept 8

im trying for this to be running in Ubuntu on .net core. In Windows this is abstracted well by the cert store with crypto provider but the same doesn't exist in Ubuntu.

Does anyone have an example of using a package that works in Ubuntu? The below is the equivalent in windows that we use to get from cert store(the cert store abstracts the TPM access)

X509Store store = new X509Store(storeName, StoreLocation.CurrentUser);  
store.Open(OpenFlags.ReadOnly);  
X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindBySubjectName, commonName,false);

I also tried this example(#94493) with the 0x8100002 reference but it fails as well

private static X509Certificate2 GetClientCert(string path, ???)
{
    using SafeEvpPKeyHandle keyHandle = ???;
    using RSAOpenSsl rsa = new RSAOpenSsl(keyHandle);
    using X509Certificate2 certOnly = new X509Certificate2(path);

    return certOnly.CopyWithPrivateKey(rsa);
}
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Oct 25, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-security, @bartonjs, @vcsjones
See info in area-owners.md if you want to be subscribed.

@bartonjs
Copy link
Member

bartonjs commented Oct 25, 2024

Since you're using an engine (and not a provider) you should be able to do it with .NET 8+

using (SafeEvpPKeyHandle keyHandle = SafeEvpPKeyHandle.OpenPrivateKeyFromEngine("tpm2tss", "0x8100002"))
using (RSA rsa = new RSAOpenSsl(keyHandle))
using (X509Certificate2 certOnly = new X509Certificate2(path))
{
    return certOnly.CopyWithPrivateKey(rsa);
}

If instead you need an OSSL_PROVIDER (instead of an ENGINE) then you need .NET 9+, where it would look like

using (SafeEvpPKeyHandle keyHandle = SafeEvpPKeyHandle.OpenKeyFromProvider("tpm2tss", "0x8100002"))
using (RSA rsa = new RSAOpenSsl(keyHandle))
using (X509Certificate2 certOnly = X509CertificateLoader.LoadCertificateFromFile(path))
{
    return certOnly.CopyWithPrivateKey(rsa);
}

@bartonjs bartonjs added the question Answer questions and provide assistance, not an issue with source code or documentation. label Oct 25, 2024
@bartonjs bartonjs added this to the Future milestone Oct 25, 2024
@bartonjs bartonjs removed the untriaged New issue has not been triaged by the area owner label Oct 25, 2024
@appcodr
Copy link
Author

appcodr commented Oct 28, 2024

so im using .net 8 and openssl version 1.1.1 and have this error when using the engine option as listed above

Unhandled exception. Interop+Crypto+OpenSslCryptographicException: error:13000074:engine routines::no such engine

and with openssl we have the enginer availalbe
openssl engine -t -c tpm2tss (tpm2tss) TPM2-TSS engine for OpenSSL [RSA, RAND] [ available ]

Is there any other config needed to use this code
using (SafeEvpPKeyHandle keyHandle = SafeEvpPKeyHandle.OpenPrivateKeyFromEngine("tpm2tss", "0x8100002")) using (RSA rsa = new RSAOpenSsl(keyHandle)) using (X509Certificate2 certOnly = new X509Certificate2(path)) { return certOnly.CopyWithPrivateKey(rsa); }

@bartonjs
Copy link
Member

Nothing special extra should be required. The argument passed to openssl engine gets passed to ENGINE_by_id, and that's the same as we do with the engineName parameter.

im using .net 8 and openssl version 1.1.1

Is there more than one copy of OpenSSL on your system? (Specifically, libssl.so and libcrypto.so). .NET is capable of using 1.1.1, but if we find 3.0 we prefer it. So if you have a libssl.so.3/libcrypto.so.3 then .NET is almost certainly choosing that version. You can ask what version we got by querying SafeEvpPKeyHandle.OpenSslVersion and printing that in hex... or use a debugger to see what specific copy of libcrypto.so got loaded.

If you have more than one, it's possible that the version we're binding to has a different ENGINE resolver path than the one you're interacting with using the commandline tool.

--

The next thing I can think of is that the ENGINE might be loading successfully, but the ENGINE is incorrectly reporting "no such engine" instead of "no such key" if the key name needs to be formatted in a manner other than what I speculated. That would probably require using a native debugger and seeing that our call to ENGINE_by_id succeeded, but our call to ENGINE_load_private_key failed. We just report whatever error OpenSSL and/or the ENGINE reported, I can't tell you if it's, y'know, accurate.

@GuyWithDogs
Copy link

My understanding is that engines are deprecated, but not removed, from OpenSSL 3 (which wants providers, but that support isn't available in .NET 8 and will come in .NET 9). However, the TPM2TSS engine installs in engines-1.1 rather than engines-3. Any idea how to get SafeEvpPKeyHandle.OpenPrivateKeyFromEngine("tpm2tss", "0x8100002") to be looking/loading the older engine using OpenSSL 3?

@bartonjs
Copy link
Member

I don't know if ENGINEs built against OSSL 1.1 will successfully load in OSSL 3. OSSL 3 changed a lot of the ABI, and while the ENGINE registration and callbacks might be stable, if they call into any other OSSL functions they might have issues. So, I've never tried to get OSSL3 to load from the engines-1.1 path, and don't know how to do it.

If you have both OSSL3 and OSSL1.1 and want .NET to load the older one, you can set an environment variable: export CLR_OPENSSL_VERSION_OVERRIDE=1.1 (assuming your OpenSSL 1.1 has files named libssl.so.1.1 and libcrypto.so.1.1; if they were libcrypto.so.11 then you'd set the environment variable to 11 (whatever is after the .so.)

@GuyWithDogs
Copy link

That export command did seem to help the program access the engine, but the error changed to

Unhandled exception. Interop+Crypto+OpenSslCryptographicException: error:26096080:engine routines:ENGINE_load_private_key:failed loading private key

That's using the SafeEvpPKeyHandle.OpenPrivateKeyFromEngine("tpm2tss", "0x8100003") form of the call. Any idea on why that might fail?

@GuyWithDogs
Copy link

An update on the same system: looks like the openssl utility does work with the handle from the command line. But, as shown above, that same handle can't be read by the .NET program.

sudo sh
# export CLR_OPENSSL_VERSION_OVERRIDE=1.1
# openssl s_server -cert rsa.crt -key 0x81000003 -keyform engine -engine tpm2tss -accept 8
engine "tpm2tss" set.
Enter password for user key:
Using default temp DH parameters
ACCEPT
# openssl version -a
OpenSSL 1.1.1 11 Sep 2018
built on: Tue Sep 24 17:37:19 2024 UTC
platform: linux-x86_64
options: bn(64,64) rc4(16x,int) des(int) idea(int) blowfish(ptr)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall -O3 -DOPENSSL_USE_NODELETE -DL_ENDIAN -DOPENSSL_PIC -DOPENSSL_CPUID_OBJ -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DKECCAK1600_ASM -DRC4_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DGHASH_ASM -DECP_NISTZ256_ASM -DX25519_ASM -DPADLOCK_ASM -DPOLY1305_ASM -DNDEBUG
OPENSSLDIR: "/usr/local/ssl"
ENGINESDIR: "/usr/local/lib/engines-1.1"
Seeding source: os-specific

@bartonjs
Copy link
Member

Your commandline information suggests you got password/PIN challenged, which the .NET API can't handle. Perhaps tpm2tss wanted to report "password required" and that got lost in "loading the key didn't work"?

@GuyWithDogs
Copy link

There's no password on the handle. In that script above, only "Enter" was pressed. At least the routine that created the private key at that handle did not explicitly apply a password to it.

@GuyWithDogs
Copy link

GuyWithDogs commented Oct 31, 2024

This is the script that was run to create the key/handle that is the target we want:

# Step 1: Generate a private key in the TPM (RSA 2048)
echo "Generating RSA private key in TPM..."
tpm2_createprimary -C o -c primary.ctx

echo "------------tpm2_create -------------"
tpm2_create -C primary.ctx -G rsa -u key.pub -r key.priv 

echo "------------tpm2_load -------------"
tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx

# Make the key persistent in the TPM
echo "------------tpm2_evictcontrol -------------"
tpm2_evictcontrol -C o -c key.ctx 0x81000003

@appcodr
Copy link
Author

appcodr commented Nov 5, 2024

@bartonjs is there an option to pass the empty password in the SafeEvpPKeyHandle.OpenPrivateKeyFromEngine ? I dont see that based on the documentation but based on how the openssl response is where it expects an empty password, do we need to pass that in from .net as well ?

@bartonjs
Copy link
Member

bartonjs commented Nov 7, 2024

is there an option to pass the empty password in the SafeEvpPKeyHandle.OpenPrivateKeyFromEngine?

No, as you noted it doesn't take any options beyond the ENGINE name and the key name.

To identify that this is a null-vs-empty problem, you'll probably have to write a test directly against OpenSSL. It would look something like the following (which notably never cleans up anything it allocated)... which I wrote in this text box, so likely doesn't compile, but it's a start.

int main(int argc, const char** argc)
{
  OPENSSL_init_ssl(
    OPENSSL_INIT_ADD_ALL_CIPHERS |
      OPENSSL_INIT_ADD_ALL_DIGESTS |
      OPENSSL_INIT_LOAD_CONFIG |
      OPENSSL_INIT_NO_ATEXIT |
      OPENSSL_INIT_LOAD_CRYPTO_STRINGS |
      OPENSSL_INIT_LOAD_SSL_STRINGS,
    NULL);

  ENGINE* engine = ENGINE_by_id("tpm2tss");
  EVP_PKEY* pkey = NULL;

  if (!engine)
  {
    printf("Engine did not load\n");
    ERR_print_errors_fp(stdout);
    return 1;
  }

  if (!ENGINE_init(engine))
  {
    printf("ENGINE_init failed.\n");
    ERR_print_errors_fp(stdout);
    return 1;
  }

  pkey = ENGINE_load_private_key(engine, "0x81000003", NULL, NULL);

  if (pkey)
  {
    printf("Key loaded the easy way, hurrah!\n");
    return 0;
  }

  printf("Key did not load the easy way.\n");
  ERR_print_errors_fp(stdout);
  printf("\n\n\n");

  UI_METHOD* uimeth = UI_null();

  pkey = ENGINE_load_private_key(engine, "0x81000003", uimeth, NULL);

  if (pkey)
  {
    printf("Key loaded with UI_null\n");
    return 0;
  }

  printf("Key did not load with UI_null.\n");
  ERR_print_errors_fp(stdout);
  printf("\n\n\n");

  uimeth = UI_openssl();

  pkey = ENGINE_load_private_key(engine, "0x81000003", uimeth, NULL);

  if (pkey)
  {
    printf("Key loaded with UI_openssl\n");
    return 0;
  }

  printf("Key did not load with UI_openssl.\n");
  ERR_print_errors_fp(stdout);
  printf("\n\n\n");

  // Exercise to the reader: If you make it this far, you can try
  // creating a custom UI_METHOD based on
  // https://github.com/openssl/openssl/blob/b372b1f76450acdfed1e2301a39810146e28b02c/apps/apps.c#L183-L275

  printf("Key never loaded :(\n");
  return 1;
}

@vcsjones
Copy link
Member

vcsjones commented Nov 8, 2024

Let me see if I can reproduce this and figure out what is going on.

@vcsjones
Copy link
Member

vcsjones commented Nov 8, 2024

Okay, I got as far as @GuyWithDogs (great handle btw)

WARNING:esys:src/tss2-esys/api/Esys_ReadPublic.c:320:Esys_ReadPublic_Finish() Received TPM Error
ERROR:esys:src/tss2-esys/esys_tr.c:231:Esys_TR_FromTPMPublic_Finish() Error ReadPublic ErrorCode (0x00000084)
ERROR:esys:src/tss2-esys/esys_tr.c:321:Esys_TR_FromTPMPublic() Error TR FromTPMPublic ErrorCode (0x00000084)
Unhandled exception. Interop+Crypto+OpenSslCryptographicException: error:26096080:engine routines:ENGINE_load_private_key:failed loading private key
   at Interop.Crypto.LoadPrivateKeyFromEngine(String engineName, String keyName)
   at Program.<Main>$(String[] args) in /home/vcsjones/Projects/Program.cs:line 2

@vcsjones
Copy link
Member

vcsjones commented Nov 8, 2024

The problem is that we are passing in NULL for the ui_method here (the 3rd parameter to the function pointer):

ret = load_func(engine, keyName, NULL, NULL);

the UI_METHOD is passed as-is to the engine here

https://github.com/openssl/openssl/blob/b10cfd93fd58cc1e9c876be159253b5389dc11a5/crypto/engine/eng_pkey.c#L77

The tpm2-tss-engine rejects that, here:

https://github.com/tpm2-software/tpm2-tss-engine/blob/3d010240b5afbabbf54c35d6c0f6e92ed0a0c0ea/src/tpm2-tss-engine.c#L80-L83

The reason it works fro the OpenSSL command line is because it uses UI_get_default_method, which unless someone changed, is UI_OpenSSL, so it passes the check in the tpm2tss engine.


Slightly annoyingly, it's not strictly safe to pass UI_null. That returns a const UI_METHOD* whereas the engine loading API wants a non-const one, presumably because a "bad" engine could make the null engine do non-null things on the singleton.

@bartonjs
Copy link
Member

bartonjs commented Nov 8, 2024

@vcsjones I'm fine with making a UI_METHOD that "just doesn't do anything" if you think that's the best path forward. Or maybe we can convince tpm2tss to not call that an invalid state? 😄

@vcsjones vcsjones removed the question Answer questions and provide assistance, not an issue with source code or documentation. label Nov 9, 2024
@vcsjones vcsjones self-assigned this Nov 9, 2024
@appcodr
Copy link
Author

appcodr commented Nov 9, 2024

Thanks @vcsjones for analyzing this further. we are happy to try this if you have a fix from a build or someway for us to try as we got stuck on this problem for a while. Its very promising that you got to the root cause!

@vcsjones
Copy link
Member

vcsjones commented Nov 9, 2024

@appcodr in order to give you a test build of the native library I would need to know more about your environment and .NET version. The full output from dotnet --info would be helpful.

@appcodr
Copy link
Author

appcodr commented Nov 10, 2024

@vcsjones I will grab that info on the target machine probably tomorrow. But its on .net 8 on Ubuntu 22.x on a x86 based processor. we tried as self contained app. our desire is to run on .net 8

@appcodr
Copy link
Author

appcodr commented Nov 11, 2024

@vcsjones Here is the info from the target machine where we want to run

$ dotnet --info
 
Host:
  Version:      8.0.10
  Architecture: x64
  Commit:       81cabf2857
  RID:          ubuntu.22.04-x64
 
.NET SDKs installed:
  No SDKs were found.
 
.NET runtimes installed:
  Microsoft.AspNetCore.App 8.0.10 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.35 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
  Microsoft.NETCore.App 8.0.10 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
 
Other architectures found:
  None
 
Environment variables:
  DOTNET_ROOT       [/home/dfs/.dotnet]
 
global.json file:
  Not found
 
Learn more:
https://aka.ms/dotnet/info
 
Download .NET:
https://aka.ms/dotnet/download

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

No branches or pull requests

4 participants