-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
When a key is available via an ENGINE (or the new OSSL3 provider model) by name, it currently requires a couple of fussy P/Invokes.
Proposal
namespace System.Security.Cryptography
{
public partial class SafeEvpPKeyHandle
{
// OpenSSL ENGINEs (only plugin model for OSSL 1.1, deprectated (but present) in 3.0+)
public static SafeEvpPKeyHandle OpenPrivateKeyFromEngine(string engineName, string keyId) => throw null;
public static SafeEvpPKeyHandle OpenPublicKeyFromEngine(string engineName, string keyId) => throw null;
// EDIT (krwq): this part got approved during API review but got cut due to issues with tpm provider - see comments
// OpenSSL Providers (new plugin model for OSSL 3.0+)
// public static SafeEvpPKeyHandle OpenKeyFromProvider(string providerName, string keyUri) => throw null;
}
}Points worth mentioning
- OpenSSL Engines (deprecated in OpenSSL 3.0) make a distinction between loading private keys and loading public keys.
- Since "load public" and "load private" are different registration points, supporting one does not imply the other, and there's nothing saying that
load_public("some key")and `load_private("some key") are related.
- Since "load public" and "load private" are different registration points, supporting one does not imply the other, and there's nothing saying that
- OpenSSL Engines use the verb "load" here.
- Windows CNG uses the verb "Open", and .NET CngKey uses "Open"
- OpenSSL 3 has a very different model of loading keys
- They're by "URI" (with provider-dependent schemes). For example, the
tpm2provider supports key URIs like "handle:0x81000000" and "object:". - There's no "from this provider" key load, but you can build a context filter to limit the interrogations to specific providers (and thus to specifically one).
- The API doesn't make a distinction from loading private keys or public keys (or non-certificates, )
- A single URI can return multiple objects. We have to make Decisions. (That decision is just like with PFX: first private ?? first public ?? throw).
- They're by "URI" (with provider-dependent schemes). For example, the
- OpenSSL 3's verb is "open" (or/also "attach" when opening from stdin makes sense)
- We don't have a good way of expressing what kind of key the key is (RSA, DSA, etc)... that's an exercise left to the caller.
Usage examples
RSA Public key from ENGINE
byte[] data = ...;
byte[] signature = ...;
using (SafeEvpPKeyHandle pubKeyHandle = SafeEvpPKeyHandle.OpenPublicKeyFromEngine("yourEngineName", "someKeyId"))
using (RSA rsaPub = new RSAOpenSsl(pubKeyHandle))
{
if (!rsaPub.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss))
{
// handle bad signature
}
}RSA Private key from ENGINE
byte[] data = ...;
using (SafeEvpPKeyHandle priKeyHandle = SafeEvpPKeyHandle.OpenPrivateKeyFromEngine("yourEngineName", "someKeyId"))
using (RSA rsaPri = new RSAOpenSsl(priKeyHandle))
{
byte[] signature = rsaPri.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
// do something with signature
}RSA Private key from Provider
byte[] data = ...;
// For TPM settings refer to provider documentation you're using, i.e. https://github.com/tpm2-software/tpm2-openssl/tree/master
// opening key might look like: SafeEvpPKeyHandle.OpenKeyFromProvider("tpm2", "handle:0x81000000")
using (SafeEvpPKeyHandle priKeyHandle = SafeEvpPKeyHandle.OpenKeyFromProvider("yourProviderName", "someKeyUri"))
using (RSA rsaPri = new RSAOpenSsl(priKeyHandle))
{
byte[] signature = rsaPri.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
// do something with signature
}Original speculation
The easiest option, is to load just the key handle, a la
namespace System.Security.Cryptography
{
partial class SafeEvpPKeyHandle
{
public static SafeEvpPKeyHandle LoadKey(string providerName, string keyId) => throw null;
}
}which turns into something like
ENGINE* e = ENGINE_by_id(providerName);
EVP_PKEY* key = NULL;
if (e != NULL)
{
if (ENGINE_init(e))
{
key = ENGINE_load_private_key(e, keyId, NULL, NULL);
ENGINE_finish(e);
}
}
return key;That needs to be reconciled with OSSL3 providers to make sure that two strings is sufficient.
Loading just the EVP_PKEY better enables the scenario of loading a key from an HSM (or other ENGINE-based system), but does still require a bit of a dance with a certificate (when applicable).
using (X509Certificate2 pubCert = ...)
using (SafeEvpPKeyHandle keyHandle = SafeEvpPKeyHandle.LoadKey(...))
{
switch (pubCert.GetKeyAlgorithm())
{
case Oids.Rsa:
using (RSAOpenSsl rsa = new RSAOpenSsl(keyHandle))
{
return pubCert.CopyWithPrivateKey(rsa);
}
case Oids.Dsa:
...
case Oids.Ecc:
case Oids.Ecdh:
{
using (ECDiffieHellmanOpenSsl ecdh = ...)
...
}
default:
throw something;
}
}So perhaps we can find a better way of doing that. Especially since the CopyWithPrivateKey implementation eventually throws away the wrapper type and just saves a copy of the key handle.
That helper operation should fail on macOS (assuming it's tied to the OpenSSL library), since macOS can't bind OpenSSL keys without exporting them.