🌀 CryptoRistretto — Operations on the Ristretto255 group
Ristretto255 provides a safer and simpler way to work with elliptic curves — specifically Curve25519 — by eliminating subtle issues and vulnerabilities that arise when using the curve directly.
It removes concerns like cofactors, low-order points, and invalid encodings, giving you a clean, prime-order group that's easier to reason about and safer to use in real-world protocols.
If you're building key exchanges, anonymous credentials, signatures, or zero-knowledge systems, Ristretto255 offers a robust foundation.
LibSodium.Net exposes the full Ristretto255 API from libsodium in a clear and idiomatic way.
📏 Constants
| Constant | Value | Description |
|---|---|---|
PointLen |
32 | Length of a Ristretto255 encoded point |
ScalarLen |
32 | Length of a scalar |
HashLen |
64 | Input length for deriving points from a hash |
NonReducedScalarLen |
64 | Input length for reduction to scalar mod L |
✨ API Surface
| Method | Description |
|---|---|
GenerateRandomScalar |
Fills a buffer with a random 32-byte scalar (modulo group order). |
GenerateRandomPoint |
Fills a buffer with a uniformly random valid Ristretto point. |
ReduceScalar |
Reduces a 64-byte value to a 32-byte scalar mod group order. |
DerivePointFromHash |
Maps a 64-byte hash to a valid Ristretto point. |
IsValidPoint |
Returns true if the point is a valid Ristretto255 encoding. |
NegateScalar |
Computes the additive inverse of a scalar. |
ComplementScalar |
Computes (L - 1 - s) mod L, where L is the group order. |
InvertScalar |
Computes the multiplicative inverse of a scalar mod group order. |
AddScalars |
Adds two scalars modulo group order. |
SubtractScalars |
Subtracts one scalar from another modulo group order. |
MultiplyScalars |
Multiplies two scalars (mod group order). |
AddPoints |
Adds two Ristretto points. |
SubtractPoints |
Subtracts one Ristretto point from another. |
ScalarMultiply |
Multiplies a point by a scalar. |
ScalarMultiplyBase |
Multiplies the base point by a scalar (used to compute public keys). |
📋 Examples
Generate a random Ristretto255 point:
Span<byte> point = stackalloc byte[CryptoRistretto.PointLen];
CryptoRistretto.GenerateRandomPoint(point);
Generate a random scalar and compute a public key:
using var sk = new SecureMemory<byte>(CryptoRistretto.ScalarLen);
SecureMemory<byte> pk = SecureMemory.Create<byte>(CryptoRistretto.PointLen);
CryptoRistretto.GenerateRandomScalar(sk);
CryptoRistretto.ScalarMultiplyBase(sk, pk);
Derive a point from a hash:
Span<byte> hash = stackalloc byte[CryptoRistretto.HashLen];
RandomNumberGenerator.Fill(hash);
Span<byte> point = stackalloc byte[CryptoRistretto.PointLen];
CryptoRistretto.DerivePointFromHash(hash, point);
Multiply two scalars:
Span<byte> k1 = stackalloc byte[32];
Span<byte> k2 = stackalloc byte[32];
Span<byte> product = stackalloc byte[32];
RandomNumberGenerator.Fill(k1);
RandomNumberGenerator.Fill(k2);
CryptoRistretto.MultiplyScalars(k1, k2, product);
Add two points:
Span<byte> p1 = stackalloc byte[32];
Span<byte> p2 = stackalloc byte[32];
Span<byte> sum = stackalloc byte[32];
CryptoRistretto.GenerateRandomPoint(p1);
CryptoRistretto.GenerateRandomPoint(p2);
CryptoRistretto.AddPoints(p1, p2, sum);
Reduce a 64-byte value to a scalar:
Useful for converting hashes, nonces, or user inputs into a valid scalar mod L.
Span<byte> longValue = stackalloc byte[64];
Span<byte> reduced = stackalloc byte[32];
RandomNumberGenerator.Fill(longValue);
CryptoRistretto.ReduceScalar(longValue, reduced);
🔑 Authenticated Key Exchange and Encryption
This example shows how Alice and Bob can use Ristretto255 and HKDF to derive a shared secret S = n₁·P₂ = n₂·P₁ from a Diffie-Hellman exchange.
The computed shared secret is then used to derive a symmetric key using HKDF. The derivation uses:
ikm = S(the shared secret point from scalar multiplication)salt = senderPublicKeyinfo = recipientPublicKey
This ensures the derived key is uniquely bound to both the shared secret and the roles of the participants.
The symmetric key is used to encrypt a message from Alice to Bob with XChaCha20-Poly1305.
🤔 Tip: Swapping
saltandinfowill produce a different key.
Be consistent across sender and recipient (e.g., always use sender's public key as salt).
This pattern prevents accidental key reuse and mitigates reflection or unknown key share attacks by incorporating both parties' public keys into the derivation process. It also allows secure, authenticated, and forward-secret communication using ephemeral keys.
using var aliceScalarSecret = new SecureMemory<byte>(CryptoRistretto.ScalarLen);
using var bobScalarSecret = new SecureMemory<byte>(CryptoRistretto.ScalarLen);
CryptoRistretto.GenerateRandomScalar(aliceScalarSecret);
CryptoRistretto.GenerateRandomScalar(bobScalarSecret);
Span<byte> alicePublicPoint = stackalloc byte[CryptoRistretto.PointLen];
Span<byte> bobPublicPoint = stackalloc byte[CryptoRistretto.PointLen];
CryptoRistretto.ScalarMultiplyBase(aliceScalarSecret, alicePublicPoint);
CryptoRistretto.ScalarMultiplyBase(bobScalarSecret, bobPublicPoint);
using var aliceSharedSecret = new SecureMemory<byte>(CryptoRistretto.PointLen);
using var bobSharedSecret = new SecureMemory<byte>(CryptoRistretto.PointLen);
CryptoRistretto.ScalarMultiply(bobScalarSecret, alicePublicPoint, bobSharedSecret);
CryptoRistretto.ScalarMultiply(aliceScalarSecret, bobPublicPoint, aliceSharedSecret);
bool isSameSharedSecret = aliceSharedSecret.AsReadOnlySpan()
.SequenceEqual(bobSharedSecret.AsReadOnlySpan());
Debug.Assert(isSameSharedSecret, "The shared secrets should be equal.");
using var aliceTxKey = new SecureMemory<byte>(XChaCha20Poly1305.KeyLen);
CryptoHkdf.DeriveKey(HashAlgorithmName.SHA512, ikm: aliceSharedSecret, okm: aliceTxKey, salt: alicePublicPoint, info: bobPublicPoint);
var aliceMessageToBobPlaintext = "Hello Bob, this is Alice!"u8;
Span<byte> aliceMessageToBobCiphertext = stackalloc byte[aliceMessageToBobPlaintext.Length + XChaCha20Poly1305.MacLen + XChaCha20Poly1305.NonceLen];
XChaCha20Poly1305.Encrypt(aliceMessageToBobCiphertext, aliceMessageToBobPlaintext, aliceTxKey);
using var bobRxKey = new SecureMemory<byte>(XChaCha20Poly1305.KeyLen);
CryptoHkdf.DeriveKey(HashAlgorithmName.SHA512, ikm: bobSharedSecret, okm: bobRxKey, salt: alicePublicPoint, info: bobPublicPoint);
Span<byte> aliceMessageToBobDecrypted = stackalloc byte[aliceMessageToBobCiphertext.Length - XChaCha20Poly1305.MacLen - XChaCha20Poly1305.NonceLen];
XChaCha20Poly1305.Decrypt(aliceMessageToBobDecrypted, aliceMessageToBobCiphertext, bobRxKey);
bool isDecryptionValid = aliceMessageToBobDecrypted.SequenceEqual(aliceMessageToBobPlaintext);
Debug.Assert(isDecryptionValid, "Decrypted message should match original plaintext.");
🔢 Two-Party Computation (Oblivious Evaluation)
This example shows how two parties can evaluate a function of the form f(x, k) = p(x)·k — without revealing x to the evaluator, or k to the input holder.
Let:
- Party A holds a secret input
x - Party B holds a secret scalar
k - The goal is for A to obtain
p(x)·k, wherep(x)is a well-known hash-to-point function
Protocol steps:
A derives
p(x)fromx, generates a random scalarr, and blindsp(x):a = p(x) + B·rwhere
Bis the base point of the group.A sends
ato B. B applies its secret scalar:b = a·k = (p(x) + B·r)·k = p(x)·k + B·(r·k)B sends
bto A. A removes the blinding term:Computes
v = B·k(received or derived from B)Computes
v·(-r) = B·(-r·k)Adds it to
b:fx = b + B·(-r·k) = p(x)·k
Thus, A obtains p(x)·k without learning k, and B never sees x.
🛡️ This technique is useful in privacy-preserving protocols like anonymous credentials, voting systems, or verifiable secret sharing.
// Party A: has input x and computes p(x)
using var x = new SecureMemory<byte>(CryptoRistretto.HashLen);
RandomGenerator.Fill(x);
using var px = new SecureMemory<byte>(CryptoRistretto.PointLen);
// p(x) = hash-to-point(x)
CryptoRistretto.DerivePointFromHash(x, px);
// A blinds p(x) using random scalar r
using var r = new SecureMemory<byte>(CryptoRistretto.ScalarLen);
Span<byte> gr = stackalloc byte[CryptoRistretto.PointLen];
Span<byte> a = stackalloc byte[CryptoRistretto.PointLen];
CryptoRistretto.GenerateRandomScalar(r);
// g·r = base point scaled by r
CryptoRistretto.ScalarMultiplyBase(r, gr);
// a = p(x) + g·r (blinded point sent to B)
CryptoRistretto.AddPoints(px.AsReadOnlySpan(), gr, a);
// -------- Party B: evaluator --------
using var k = new SecureMemory<byte>(CryptoRistretto.ScalarLen);
Span<byte> v = stackalloc byte[CryptoRistretto.PointLen];
Span<byte> b = stackalloc byte[CryptoRistretto.PointLen];
CryptoRistretto.GenerateRandomScalar(k);
// v = g·k (public key of B)
CryptoRistretto.ScalarMultiplyBase(k, v);
// b = a·k = p(x)·k + g·(r·k)
CryptoRistretto.ScalarMultiply(k.AsReadOnlySpan(), a, b);
// -------- Party A: unblinding --------
using var ir = new SecureMemory<byte>(CryptoRistretto.ScalarLen);
Span<byte> vir = stackalloc byte[CryptoRistretto.PointLen];
using var fx = new SecureMemory<byte>(CryptoRistretto.PointLen);
// Compute -r
CryptoRistretto.NegateScalar(r, ir);
// Compute v·(-r) = g·(-r·k)
CryptoRistretto.ScalarMultiply(ir.AsReadOnlySpan(), v, vir);
// Recover fx = b + g·(-r·k) = p(x)·k
CryptoRistretto.AddPoints(b, vir, fx.AsSpan());
// Validate that fx == p(x)·k
using var expected = new SecureMemory<byte>(CryptoRistretto.PointLen);
CryptoRistretto.ScalarMultiply(k, px, expected);
bool isValid = fx.AsReadOnlySpan().SequenceEqual(expected.AsReadOnlySpan());
Debug.Assert(isValid, "The final result fx should match the expected value.");
💡 This protocol can serve as a building block for secure multiparty computation or threshold cryptography.