Background and motivation
I was noodling with the new X25519DiffieHellman API and there is is an opportunity to provide an API shape that will help with performance.
The overwhelming use of X25519 is for ephemeral key agreement - a single DeriveRawSecretAgreement is made, which creates a shared key (to be put in to a KDF) and then the keys are thrown away.
In the case of our current API shape, it assumes that the keys are instances of X25519DiffieHellman, public or private. Roughly, that looks like this today:
using X25519DiffieHellman me = X25519DiffieHellman.GenerateKey();
AuthenticateAndSendOtherPartyMyPublicKey(me.ExportPublicKey());
ReadOnlySpan<byte> otherPartyPublicKey = AuthenticateAndGetOtherPartyPublicKeyOverWire();
X25519DiffieHellman peer = X25519DiffieHellman.ImportPublicKey(otherPartyPublicKey);
byte[] secret = me.DeriveRawSecretAgreement(peer);
peer.Dispose();
We basically take our peer's public key, import it, use it once, then dispose. That has some overhead
- A p/invoke to create a platform's native representation of the handle
- Allocating an instance of
SafeHandle
- Allocating an instance of
X25519DiffieHellman
- A p/invoke to destroy the
SafeHandle
All of this can go away if we create a quasi one-shot of performing the key agreement. On every platform except Windows, we have a native shim, and we can keep all of the public handle parts in the shim. Even in the case of windows, the X25519DiffieHellman class allocation goes away and we will only allocate the native handles.
X25519 is also deterministic, so usually once you do a key agreement with a particular public and private key, the same answer comes out every time, so memoizing the public key doesn't seem like a likely scenario.
API Proposal
namespace System.Security.Cryptography
public partial class X25519DiffieHellman
{
public byte[] DeriveRawSecretAgreement(byte[] otherPartyPublicKey);
public void DeriveRawSecretAgreement(ReadOnlySpan<byte> otherPartyPublicKey, Span<byte> destination);
protected abstract void DeriveRawSecretAgreementCore(ReadOnlySpan<byte> otherPartyPublicKey, Span<byte> destination);
}
API Usage
Our pseudo code from above becomes
using X25519DiffieHellman me = X25519DiffieHellman.GenerateKey();
AuthenticateAndSendOtherPartyMyPublicKey(me.ExportPublicKey());
ReadOnlySpan<byte> otherPartyPublicKey = AuthenticateAndGetOtherPartyPublicKeyOverWire();
byte[] secret = me.DeriveRawSecretAgreement(otherPartyPublicKey);
Alternative Designs
-
We do nothing. This is a performance focused API, but it contributes to a common use case of X25519 which is ephemeral key agreement and minimizing allocations.
-
We considered making this virtual instead of abstract and the virtual implementation would use the existing one, but virtual comes with its own set of problems, particularly that methods must always continue to call the same virtual "chain", otherwise it is a breaking change. We generally expect few subtypes of this class, so asking users to implement both feels reasonable. And they can always implement one in terms of the other, if they want to.
Risks
More complexity for an API that does not unblock any scenarios, just makes a common scenario allocate less and better throughput.
Background and motivation
I was noodling with the new X25519DiffieHellman API and there is is an opportunity to provide an API shape that will help with performance.
The overwhelming use of X25519 is for ephemeral key agreement - a single
DeriveRawSecretAgreementis made, which creates a shared key (to be put in to a KDF) and then the keys are thrown away.In the case of our current API shape, it assumes that the keys are instances of
X25519DiffieHellman, public or private. Roughly, that looks like this today:We basically take our peer's public key, import it, use it once, then dispose. That has some overhead
SafeHandleX25519DiffieHellmanSafeHandleAll of this can go away if we create a quasi one-shot of performing the key agreement. On every platform except Windows, we have a native shim, and we can keep all of the public handle parts in the shim. Even in the case of windows, the X25519DiffieHellman class allocation goes away and we will only allocate the native handles.
X25519 is also deterministic, so usually once you do a key agreement with a particular public and private key, the same answer comes out every time, so memoizing the public key doesn't seem like a likely scenario.
API Proposal
API Usage
Our pseudo code from above becomes
Alternative Designs
We do nothing. This is a performance focused API, but it contributes to a common use case of X25519 which is ephemeral key agreement and minimizing allocations.
We considered making this
virtualinstead ofabstractand the virtual implementation would use the existing one, butvirtualcomes with its own set of problems, particularly that methods must always continue to call the same virtual "chain", otherwise it is a breaking change. We generally expect few subtypes of this class, so asking users to implement both feels reasonable. And they can always implement one in terms of the other, if they want to.Risks
More complexity for an API that does not unblock any scenarios, just makes a common scenario allocate less and better throughput.