Skip to content

[API Proposal]: X25519DiffieHellman.DeriveRawSecretAgreement with bytes public key #128040

@vcsjones

Description

@vcsjones

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

  1. A p/invoke to create a platform's native representation of the handle
  2. Allocating an instance of SafeHandle
  3. Allocating an instance of X25519DiffieHellman
  4. 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

  1. 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions