Skip to content

Add an easier way of opening named keys via OpenSSL #55356

@bartonjs

Description

@bartonjs

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.
  • 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 tpm2 provider 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).
  • 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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions