Add project files.

This commit is contained in:
micro
2026-06-20 09:31:59 -05:00
parent 9bb3f00d86
commit 4baa4ce8c0
346 changed files with 55751 additions and 0 deletions
@@ -0,0 +1,90 @@
using Wingnal.Protocol.ZkGroup.Curve;
using Wingnal.Protocol.ZkGroup.Poksho;
namespace Wingnal.Protocol.ZkGroup.ZkCredential;
/// <summary>
/// Port of libsignal's <c>zkcredential::attributes</c> verifiable-encryption layer (Chase-Perrin-Zaverucha
/// §4.1). An attribute is a pair of Ristretto points (M1, M2). A <see cref="AttributeKeyPair"/> holds two
/// scalars (a1, a2); encryption is <c>E_A1 = a1·M1; E_A2 = a2·E_A1 + M2</c>. The verifying server can match
/// the ciphertext without learning the plaintext, which is how a group hides its members' ACIs/profile keys.
/// </summary>
public readonly struct AttributeCiphertext
{
public readonly Ristretto255 EA1;
public readonly Ristretto255 EA2;
public AttributeCiphertext(Ristretto255 ea1, Ristretto255 ea2) { EA1 = ea1; EA2 = ea2; }
/// <summary>The ciphertext as its own attribute (for chaining), per zkcredential.</summary>
public Ristretto255[] AsPoints() => new[] { EA1, EA2 };
/// <summary>64-byte serialization: E_A1‖E_A2 (each a 32-byte compressed Ristretto point).</summary>
public byte[] Serialize()
{
var b = new byte[64];
Array.Copy(EA1.Encode(), 0, b, 0, 32);
Array.Copy(EA2.Encode(), 0, b, 32, 32);
return b;
}
public static AttributeCiphertext Deserialize(ReadOnlySpan<byte> bytes64)
{
if (bytes64.Length != 64) throw new ArgumentException("ciphertext must be 64 bytes");
Ristretto255 ea1 = Ristretto255.Decode(bytes64[..32]) ?? throw new ArgumentException("bad E_A1");
Ristretto255 ea2 = Ristretto255.Decode(bytes64[32..64]) ?? throw new ArgumentException("bad E_A2");
return new AttributeCiphertext(ea1, ea2);
}
}
/// <summary>
/// A key for encrypting one kind of attribute (a domain). The private key is (a1, a2); the public key is
/// A = a1·G_a1 + a2·G_a2. Different domains use different generator points so ciphertexts can't be confused.
/// </summary>
public sealed class AttributeKeyPair
{
public Scalar25519 A1 { get; }
public Scalar25519 A2 { get; }
public Ristretto255 PublicKey { get; } // A
private AttributeKeyPair(Scalar25519 a1, Scalar25519 a2, Ristretto255 publicKey)
{
A1 = a1; A2 = a2; PublicKey = publicKey;
}
/// <summary>Derives a deterministic key pair from the SHO state and the domain's generator points.</summary>
public static AttributeKeyPair DeriveFrom(ShoHmacSha256 sho, Ristretto255 ga1, Ristretto255 ga2)
{
Scalar25519 a1 = Scalar25519.FromBytesModOrderWide(sho.SqueezeAndRatchet(64));
Scalar25519 a2 = Scalar25519.FromBytesModOrderWide(sho.SqueezeAndRatchet(64));
Ristretto255 a = Ristretto255.Add(ga1.Multiply(a1), ga2.Multiply(a2));
return new AttributeKeyPair(a1, a2, a);
}
public static AttributeKeyPair FromScalars(Scalar25519 a1, Scalar25519 a2, Ristretto255 ga1, Ristretto255 ga2)
=> new(a1, a2, Ristretto255.Add(ga1.Multiply(a1), ga2.Multiply(a2)));
/// <summary>Encrypts an attribute (M1, M2): E_A1 = a1·M1; E_A2 = a2·E_A1 + M2.</summary>
public AttributeCiphertext Encrypt(Ristretto255 m1, Ristretto255 m2)
{
Ristretto255 ea1 = m1.Multiply(A1);
Ristretto255 ea2 = Ristretto255.Add(ea1.Multiply(A2), m2);
return new AttributeCiphertext(ea1, ea2);
}
/// <summary>Recovers M2 = E_A2 a2·E_A1. Throws if E_A1 is the basepoint (a1 not actually encrypting).
/// The caller MUST verify the decoded value re-encrypts to E_A1 (decode is otherwise garbage-in/out).</summary>
public Ristretto255 DecryptToSecondPoint(AttributeCiphertext ct)
{
if (ct.EA1.ConstantTimeEquals(Ristretto255.BasePoint))
throw new ZkGroupVerificationException("E_A1 is the basepoint");
Ristretto255 a2EA1 = ct.EA1.Multiply(A2);
return Ristretto255.Add(ct.EA2, Ristretto255.Negate(a2EA1));
}
}
/// <summary>A zkgroup verification failure (a wrong key, forged ciphertext, or invalid proof).</summary>
public sealed class ZkGroupVerificationException : Exception
{
public ZkGroupVerificationException(string message) : base(message) { }
}
@@ -0,0 +1,158 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Text;
using Wingnal.Protocol.ZkGroup.Curve;
using Wingnal.Protocol.ZkGroup.Poksho;
namespace Wingnal.Protocol.ZkGroup.ZkCredential;
/// <summary>
/// Issuance + presentation proofs for the zkcredential MAC system (Chase-Perrin-Zaverucha §3.2 / §4.1),
/// expressed as poksho <see cref="Statement"/>s (reusing the byte-exact Schnorr engine). The client uses
/// <see cref="IssuanceProofBuilder"/> to verify a server-issued credential and
/// <see cref="PresentationProofBuilder"/> to build the anonymous presentation it sends to the verifying
/// (storage) server. <see cref="PresentationProofVerifier"/> is included for offline round-trip testing.
/// </summary>
public sealed class EncryptionKeyContext
{
public string Id = "";
public Ristretto255 Ga1 = Ristretto255.Identity;
public Ristretto255 Ga2 = Ristretto255.Identity;
public Scalar25519 A1, A2; // present for the prover (KeyPair)
public Ristretto255? PublicKeyA; // the encryption public key A (present when key is "verified")
}
public sealed class IssuanceProof
{
public Credential Credential = null!;
public byte[] PokshoProof = System.Array.Empty<byte>();
/// <summary>bincode: Credential(96) ‖ Vec&lt;u8&gt;(u64le len ‖ proof bytes).</summary>
public byte[] Serialize()
{
var b = new List<byte>(Credential.Serialize());
Span<byte> len = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(len, (ulong)PokshoProof.Length);
b.AddRange(len.ToArray());
b.AddRange(PokshoProof);
return b.ToArray();
}
public static IssuanceProof Deserialize(ReadOnlySpan<byte> bytes)
{
if (bytes.Length < 96 + 8) throw new ArgumentException("IssuanceProof too short");
var credential = Credential.Deserialize(bytes[..96]);
ulong len = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(96, 8));
if (96 + 8 + (int)len != bytes.Length) throw new ArgumentException("IssuanceProof length mismatch");
return new IssuanceProof { Credential = credential, PokshoProof = bytes.Slice(104, (int)len).ToArray() };
}
}
public sealed class IssuanceProofBuilder
{
private readonly ShoHmacSha256 _publicAttrs;
private readonly byte[] _message;
private readonly List<Ristretto255> _attrPoints = new() { Ristretto255.Identity }; // [0] reserved for public
public IssuanceProofBuilder(byte[] label, byte[]? message = null)
{
_publicAttrs = new ShoHmacSha256(label);
_message = message ?? System.Array.Empty<byte>();
}
public IssuanceProofBuilder AddPublicAttributeU64(ulong value)
{
Span<byte> be = stackalloc byte[8];
BinaryPrimitives.WriteUInt64BigEndian(be, value);
_publicAttrs.AbsorbAndRatchet(be); // ratchet() after is a no-op (already ratcheted)
return this;
}
public IssuanceProofBuilder AddAttribute(Ristretto255[] points)
{
_attrPoints.AddRange(points);
if (_attrPoints.Count > CredentialSystem.NumSupportedAttrs)
throw new ArgumentException("too many attribute points");
return this;
}
private void FinalizePublicAttrs() => _attrPoints[0] = _publicAttrs.GetPoint();
private Statement BuildStatement()
{
var st = new Statement();
st.Add("C_W", ("w", "G_w"), ("wprime", "G_wprime"));
var gvi = new (string, string)[]
{
("x0", "G_x0"), ("x1", "G_x1"),
("y0", "G_y0"), ("y1", "G_y1"), ("y2", "G_y2"), ("y3", "G_y3"),
("y4", "G_y4"), ("y5", "G_y5"), ("y6", "G_y6"),
};
st.Add("G_V-I", gvi[..(2 + _attrPoints.Count)]);
var vt = new (string, string)[]
{
("w", "G_w"), ("x0", "U"), ("x1", "tU"),
("y0", "M0"), ("y1", "M1"), ("y2", "M2"), ("y3", "M3"),
("y4", "M4"), ("y5", "M5"), ("y6", "M6"),
};
st.Add("V", vt[..(3 + _attrPoints.Count)]);
return st;
}
private Dictionary<string, Ristretto255> PointArgs(CredentialPublicKey key, Credential credential)
{
var s = CredentialSystem.SystemParams.Hardcoded;
var p = new Dictionary<string, Ristretto255>
{
["C_W"] = key.CW,
["G_w"] = s.GW,
["G_wprime"] = s.GWprime,
["G_V-I"] = Ristretto255.Add(s.GV, Ristretto255.Negate(key.IFor(_attrPoints.Count))),
["G_x0"] = s.GX0,
["G_x1"] = s.GX1,
["V"] = credential.V,
["U"] = credential.U,
["tU"] = credential.U.Multiply(credential.T),
};
string[] gy = { "G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6" };
for (int i = 0; i < _attrPoints.Count; i++) p[gy[i]] = s.GY[i];
string[] mn = { "M0", "M1", "M2", "M3", "M4", "M5", "M6" };
for (int i = 0; i < _attrPoints.Count; i++) p[mn[i]] = _attrPoints[i];
return p;
}
/// <summary>Verifies a server-issued credential, returning it on success.</summary>
public Credential Verify(CredentialPublicKey publicKey, IssuanceProof proof)
{
FinalizePublicAttrs();
Dictionary<string, Ristretto255> points = PointArgs(publicKey, proof.Credential);
if (!BuildStatement().VerifyProof(proof.PokshoProof, points, _message))
throw new ZkGroupVerificationException("issuance proof did not verify");
return proof.Credential;
}
/// <summary>Issues a credential (server side; used for offline tests).</summary>
public IssuanceProof Issue(CredentialKeyPair keyPair, byte[] randomness)
{
FinalizePublicAttrs();
var sho = new ShoHmacSha256(Encoding.ASCII.GetBytes("Signal_ZKCredential_Issuance_20230410"));
sho.AbsorbAndRatchet(randomness);
Credential credential = keyPair.Private.CredentialCore(_attrPoints.ToArray(), sho);
var scalars = new Dictionary<string, Scalar25519>
{
["w"] = keyPair.Private.W,
["wprime"] = keyPair.Private.Wprime,
["x0"] = keyPair.Private.X0,
["x1"] = keyPair.Private.X1,
};
string[] yn = { "y0", "y1", "y2", "y3", "y4", "y5", "y6" };
for (int i = 0; i < _attrPoints.Count; i++) scalars[yn[i]] = keyPair.Private.Y[i];
Dictionary<string, Ristretto255> points = PointArgs(keyPair.Public, credential);
byte[] poksho = BuildStatement().Prove(scalars, points, _message, sho.SqueezeAndRatchet(32));
return new IssuanceProof { Credential = credential, PokshoProof = poksho };
}
}
@@ -0,0 +1,185 @@
using System.Text;
using Wingnal.Protocol.ZkGroup.Curve;
using Wingnal.Protocol.ZkGroup.Poksho;
namespace Wingnal.Protocol.ZkGroup.ZkCredential;
/// <summary>
/// Port of libsignal's <c>zkcredential::credentials</c> — the algebraic-MAC credential system
/// (Chase-Perrin-Zaverucha §3.1) that AuthCredential/ProfileKeyCredential are built on. Supports up to
/// <see cref="NumSupportedAttrs"/> attribute points. The shared <see cref="SystemParams"/> generators are
/// derived via <see cref="ShoSha256"/> and gated against libsignal's hardcoded serialization.
/// </summary>
public static class CredentialSystem
{
public const int NumSupportedAttrs = 7; // 1 aggregate public + 3 two-point private attributes
public sealed class SystemParams
{
public Ristretto255 GW, GWprime, GX0, GX1, GV, GZ;
public Ristretto255[] GY = new Ristretto255[NumSupportedAttrs];
public static readonly SystemParams Hardcoded = Generate();
public static SystemParams Generate()
{
var sho = new ShoSha256(Encoding.ASCII.GetBytes(
"Signal_ZKCredential_ConstantSystemParams_generate_20230410"));
var p = new SystemParams
{
GW = sho.GetPoint(),
GWprime = sho.GetPoint(),
GX0 = sho.GetPoint(),
GX1 = sho.GetPoint(),
GV = sho.GetPoint(),
GZ = sho.GetPoint(),
};
for (int i = 0; i < NumSupportedAttrs; i++) p.GY[i] = sho.GetPoint();
return p;
}
/// <summary>bincode serialization: G_w‖G_wprime‖G_x0‖G_x1‖G_V‖G_z‖G_y[0..7] (13 × 32 = 416 bytes).</summary>
public byte[] Serialize()
{
var b = new byte[13 * 32];
int o = 0;
foreach (Ristretto255 p in new[] { GW, GWprime, GX0, GX1, GV, GZ })
{ Array.Copy(p.Encode(), 0, b, o, 32); o += 32; }
foreach (Ristretto255 p in GY) { Array.Copy(p.Encode(), 0, b, o, 32); o += 32; }
return b;
}
}
}
/// <summary>A credential: the MAC (t, U, V) issued by the server over a set of attributes.</summary>
public sealed class Credential
{
public Scalar25519 T;
public Ristretto255 U;
public Ristretto255 V;
public Credential(Scalar25519 t, Ristretto255 u, Ristretto255 v) { T = t; U = u; V = v; }
/// <summary>bincode: t(32)‖U(32)‖V(32) = 96 bytes.</summary>
public byte[] Serialize()
{
var b = new byte[96];
Array.Copy(T.ToBytes(), 0, b, 0, 32);
Array.Copy(U.Encode(), 0, b, 32, 32);
Array.Copy(V.Encode(), 0, b, 64, 32);
return b;
}
public static Credential Deserialize(ReadOnlySpan<byte> b)
{
if (b.Length != 96) throw new ArgumentException("credential must be 96 bytes");
Scalar25519 t = Scalar25519.FromCanonicalBytes(b[..32]) ?? throw new ArgumentException("bad t");
Ristretto255 u = Ristretto255.Decode(b[32..64]) ?? throw new ArgumentException("bad U");
Ristretto255 v = Ristretto255.Decode(b[64..96]) ?? throw new ArgumentException("bad V");
return new Credential(t, u, v);
}
}
/// <summary>The server's private credential key (only needed locally for tests / a verifying server).</summary>
public sealed class CredentialPrivateKey
{
public Scalar25519 W, Wprime, X0, X1;
public Ristretto255 BigW;
public Scalar25519[] Y = new Scalar25519[CredentialSystem.NumSupportedAttrs];
public static CredentialPrivateKey Generate(byte[] randomness)
{
var sho = new ShoHmacSha256(Encoding.ASCII.GetBytes(
"Signal_ZKCredential_CredentialPrivateKey_generate_20230410"));
sho.AbsorbAndRatchet(randomness);
var system = CredentialSystem.SystemParams.Hardcoded;
var k = new CredentialPrivateKey();
k.W = sho.GetScalar();
k.BigW = system.GW.Multiply(k.W);
k.Wprime = sho.GetScalar();
k.X0 = sho.GetScalar();
k.X1 = sho.GetScalar();
for (int i = 0; i < CredentialSystem.NumSupportedAttrs; i++) k.Y[i] = sho.GetScalar();
return k;
}
/// <summary>Produces the MAC over the attribute points (Chase-Perrin-Zaverucha §3.1).</summary>
public Credential CredentialCore(Ristretto255[] m, ShoHmacSha256 sho)
{
if (m.Length > CredentialSystem.NumSupportedAttrs) throw new ArgumentException("too many attributes");
Scalar25519 t = sho.GetScalar();
Ristretto255 u = sho.GetPoint();
// V = W + (x0 + x1·t)·U + Σ y_i·M_i
Scalar25519 coeff = Scalar25519.Add(X0, Scalar25519.Mul(X1, t));
Ristretto255 v = Ristretto255.Add(BigW, u.Multiply(coeff));
for (int i = 0; i < m.Length; i++) v = Ristretto255.Add(v, m[i].Multiply(Y[i]));
return new Credential(t, u, v);
}
}
/// <summary>The server's public credential key the client uses to receive + present credentials.</summary>
public sealed class CredentialPublicKey
{
public Ristretto255 CW;
public Ristretto255[] I = new Ristretto255[CredentialSystem.NumSupportedAttrs - 1]; // I_2 .. I_7
/// <summary>I for a credential with <paramref name="numAttrs"/> attribute points (≥2).</summary>
public Ristretto255 IFor(int numAttrs) => I[numAttrs - 2];
public static CredentialPublicKey FromPrivate(CredentialPrivateKey priv)
{
var system = CredentialSystem.SystemParams.Hardcoded;
var pub = new CredentialPublicKey
{
CW = Ristretto255.Add(priv.BigW, system.GWprime.Multiply(priv.Wprime)),
};
// I_i = G_V - x0·G_x0 - x1·G_x1 - Σ_{j≤i} y_j·G_y_j
Ristretto255 ii = system.GV;
ii = Ristretto255.Add(ii, Ristretto255.Negate(system.GX0.Multiply(priv.X0)));
ii = Ristretto255.Add(ii, Ristretto255.Negate(system.GX1.Multiply(priv.X1)));
ii = Ristretto255.Add(ii, Ristretto255.Negate(system.GY[0].Multiply(priv.Y[0])));
for (int n = 1; n < CredentialSystem.NumSupportedAttrs; n++)
{
ii = Ristretto255.Add(ii, Ristretto255.Negate(system.GY[n].Multiply(priv.Y[n])));
pub.I[n - 1] = ii;
}
return pub;
}
/// <summary>bincode: C_W(32)‖I[6]·32 = 224 bytes.</summary>
public byte[] Serialize()
{
var b = new byte[32 + 32 * (CredentialSystem.NumSupportedAttrs - 1)];
Array.Copy(CW.Encode(), 0, b, 0, 32);
for (int i = 0; i < I.Length; i++) Array.Copy(I[i].Encode(), 0, b, 32 + 32 * i, 32);
return b;
}
public static CredentialPublicKey Deserialize(ReadOnlySpan<byte> b)
{
int expected = 32 + 32 * (CredentialSystem.NumSupportedAttrs - 1);
if (b.Length != expected) throw new ArgumentException("bad CredentialPublicKey length");
var pub = new CredentialPublicKey
{
CW = Ristretto255.Decode(b[..32]) ?? throw new ArgumentException("bad C_W"),
};
for (int i = 0; i < pub.I.Length; i++)
pub.I[i] = Ristretto255.Decode(b.Slice(32 + 32 * i, 32)) ?? throw new ArgumentException("bad I");
return pub;
}
}
/// <summary>The server's credential key pair (private + derived public).</summary>
public sealed class CredentialKeyPair
{
public CredentialPrivateKey Private { get; }
public CredentialPublicKey Public { get; }
private CredentialKeyPair(CredentialPrivateKey priv, CredentialPublicKey pub) { Private = priv; Public = pub; }
public static CredentialKeyPair Generate(byte[] randomness)
{
var priv = CredentialPrivateKey.Generate(randomness);
return new CredentialKeyPair(priv, CredentialPublicKey.FromPrivate(priv));
}
}
@@ -0,0 +1,278 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Text;
using Wingnal.Protocol.ZkGroup.Curve;
using Wingnal.Protocol.ZkGroup.Poksho;
namespace Wingnal.Protocol.ZkGroup.ZkCredential;
/// <summary>A credential presentation proof (Chase-Perrin-Zaverucha §3.2/§4.1): the commitments plus the
/// poksho proof. Serialized with bincode (fixint, little-endian; Vec = u64 length prefix + elements).</summary>
public sealed class PresentationProof
{
public Ristretto255 Cx0 = null!, Cx1 = null!, Cv = null!;
public Ristretto255[] Cy = System.Array.Empty<Ristretto255>();
public byte[] PokshoProof = System.Array.Empty<byte>();
public byte[] Serialize()
{
var ms = new List<byte>();
ms.AddRange(Cx0.Encode());
ms.AddRange(Cx1.Encode());
ms.AddRange(Cv.Encode());
AddU64Le(ms, (ulong)Cy.Length);
foreach (Ristretto255 p in Cy) ms.AddRange(p.Encode());
AddU64Le(ms, (ulong)PokshoProof.Length);
ms.AddRange(PokshoProof);
return ms.ToArray();
}
private static void AddU64Le(List<byte> dst, ulong v)
{
Span<byte> b = stackalloc byte[8];
BinaryPrimitives.WriteUInt64LittleEndian(b, v);
dst.AddRange(b.ToArray());
}
}
internal struct AttrRef { public int? KeyIndex; public int First; public int Second; }
/// <summary>Builds a credential presentation (the anonymous proof sent to the verifying/storage server).</summary>
public sealed class PresentationProofBuilder
{
private readonly byte[] _message;
private readonly List<EncryptionKeyContext> _keys = new();
private readonly List<AttrRef> _attrs = new();
private readonly List<Ristretto255> _attrPoints = new() { Ristretto255.Identity };
public PresentationProofBuilder(byte[] label, byte[]? message = null)
{
_ = label; // label is ignored on the prover side (public attrs are server-provided)
_message = message ?? System.Array.Empty<byte>();
}
public PresentationProofBuilder AddAttribute(Ristretto255[] points, EncryptionKeyContext key)
{
int first = _attrPoints.Count;
_attrPoints.AddRange(points);
if (_attrPoints.Count > CredentialSystem.NumSupportedAttrs)
throw new ArgumentException("too many attribute points");
int keyIndex = _keys.FindIndex(k => k.Id == key.Id);
if (keyIndex < 0) { keyIndex = _keys.Count; _keys.Add(key); }
_attrs.Add(new AttrRef { KeyIndex = keyIndex, First = first, Second = first + points.Length - 1 });
return this;
}
public PresentationProof Present(CredentialPublicKey publicKey, Credential credential, byte[] randomness)
{
var s = CredentialSystem.SystemParams.Hardcoded;
var sho = new ShoHmacSha256(Encoding.ASCII.GetBytes("Signal_ZKCredential_Presentation_20230410"));
sho.AbsorbAndRatchet(randomness);
Scalar25519 z = sho.GetScalar();
var cy = new Ristretto255[_attrPoints.Count];
for (int i = 0; i < _attrPoints.Count; i++)
cy[i] = Ristretto255.Add(s.GY[i].Multiply(z), _attrPoints[i]);
Ristretto255 cx0 = Ristretto255.Add(s.GX0.Multiply(z), credential.U);
Ristretto255 cv = Ristretto255.Add(s.GV.Multiply(z), credential.V);
Ristretto255 cx1 = Ristretto255.Add(s.GX1.Multiply(z), credential.U.Multiply(credential.T));
Scalar25519 z0 = Scalar25519.Negate(Scalar25519.Mul(z, credential.T));
Ristretto255 ii = publicKey.IFor(_attrPoints.Count);
Ristretto255 bigZ = ii.Multiply(z);
var scalars = new Dictionary<string, Scalar25519> { ["z"] = z, ["t"] = credential.T, ["z0"] = z0 };
foreach (EncryptionKeyContext k in _keys)
{
scalars[$"a1_{k.Id}"] = k.A1;
scalars[$"a2_{k.Id}"] = k.A2;
scalars[$"z1_{k.Id}"] = Scalar25519.Negate(Scalar25519.Mul(z, k.A1));
}
Dictionary<string, Ristretto255> points = PrepareNonAttrPoints(ii, cx0, cx1, cy);
points["Z"] = bigZ;
foreach (AttrRef attr in _attrs)
{
points[$"C_y{attr.First}"] = cy[attr.First];
if (attr.KeyIndex is { } ki)
{
EncryptionKeyContext k = _keys[ki];
Ristretto255 eA1 = _attrPoints[attr.First].Multiply(k.A1);
Ristretto255 eA2 = Ristretto255.Add(eA1.Multiply(k.A2), _attrPoints[attr.Second]);
points[$"E_A{attr.First}"] = eA1;
points[$"-E_A{attr.First}"] = Ristretto255.Negate(eA1);
points[$"C_y{attr.Second}-E_A{attr.Second}"] = Ristretto255.Add(cy[attr.Second], Ristretto255.Negate(eA2));
}
}
byte[] poksho = BuildStatement(_keys, _attrs).Prove(scalars, points, _message, sho.SqueezeAndRatchet(32));
return new PresentationProof { Cx0 = cx0, Cx1 = cx1, Cv = cv, Cy = cy, PokshoProof = poksho };
}
private Dictionary<string, Ristretto255> PrepareNonAttrPoints(
Ristretto255 ii, Ristretto255 cx0, Ristretto255 cx1, Ristretto255[] cy)
{
var s = CredentialSystem.SystemParams.Hardcoded;
var p = new Dictionary<string, Ristretto255>
{
["I"] = ii, ["C_x0"] = cx0, ["C_x1"] = cx1, ["G_x0"] = s.GX0, ["G_x1"] = s.GX1,
};
if (_keys.Count > 0)
{
p["0"] = Ristretto255.Identity;
Ristretto255 sumA = Ristretto255.Identity;
bool any = false;
foreach (EncryptionKeyContext k in _keys)
{
if (k.PublicKeyA is { } a)
{
p[$"G_a1_{k.Id}"] = k.Ga1;
p[$"G_a2_{k.Id}"] = k.Ga2;
sumA = Ristretto255.Add(sumA, a);
any = true;
}
}
if (any) p["sum(A)"] = sumA;
}
string[] gy = { "G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6" };
for (int i = 0; i < _attrPoints.Count; i++) p[gy[i]] = s.GY[i];
p["C_y0"] = cy[0];
return p;
}
internal static Statement BuildStatement(List<EncryptionKeyContext> keys, List<AttrRef> attrs)
{
var st = new Statement();
st.Add("Z", ("z", "I"));
st.Add("C_x1", ("t", "C_x0"), ("z0", "G_x0"), ("z", "G_x1"));
var sumTerms = new List<(string, string)>();
foreach (EncryptionKeyContext k in keys)
{
st.Add("0", ($"z1_{k.Id}", "I"), ($"a1_{k.Id}", "Z"));
if (k.PublicKeyA is not null)
{
sumTerms.Add(($"a1_{k.Id}", $"G_a1_{k.Id}"));
sumTerms.Add(($"a2_{k.Id}", $"G_a2_{k.Id}"));
}
}
if (sumTerms.Count > 0) st.Add("sum(A)", sumTerms.ToArray());
foreach (AttrRef attr in attrs)
{
if (attr.KeyIndex is { } ki)
{
string id = keys[ki].Id;
st.Add($"E_A{attr.First}", ($"a1_{id}", $"C_y{attr.First}"), ($"z1_{id}", $"G_y{attr.First}"));
st.Add($"C_y{attr.Second}-E_A{attr.Second}",
("z", $"G_y{attr.Second}"), ($"a2_{id}", $"-E_A{attr.First}"));
}
else
{
st.Add($"C_y{attr.First}", ("z", $"G_y{attr.First}"));
}
}
st.Add("C_y0", ("z", "G_y0"));
return st;
}
}
/// <summary>Verifies a presentation (the verifying-server side; used here for offline round-trip testing).</summary>
public sealed class PresentationProofVerifier
{
private readonly ShoHmacSha256 _publicAttrs;
private readonly byte[] _message;
private readonly List<EncryptionKeyContext> _keys = new();
private readonly List<AttrRef> _attrs = new();
private readonly List<Ristretto255> _attrPoints = new() { Ristretto255.Identity };
public PresentationProofVerifier(byte[] label, byte[]? message = null)
{
_publicAttrs = new ShoHmacSha256(label);
_message = message ?? System.Array.Empty<byte>();
}
public PresentationProofVerifier AddPublicAttributeU64(ulong value)
{
Span<byte> be = stackalloc byte[8];
BinaryPrimitives.WriteUInt64BigEndian(be, value);
_publicAttrs.AbsorbAndRatchet(be);
return this;
}
/// <summary>Adds an encrypted attribute (the ciphertext points) + the public encryption key context.</summary>
public PresentationProofVerifier AddAttribute(Ristretto255[] ciphertextPoints, EncryptionKeyContext key)
{
int first = _attrPoints.Count;
_attrPoints.AddRange(ciphertextPoints);
int keyIndex = _keys.FindIndex(k => k.Id == key.Id);
if (keyIndex < 0) { keyIndex = _keys.Count; _keys.Add(key); }
_attrs.Add(new AttrRef { KeyIndex = keyIndex, First = first, Second = first + ciphertextPoints.Length - 1 });
return this;
}
public bool Verify(CredentialKeyPair keyPair, PresentationProof proof)
{
_attrPoints[0] = _publicAttrs.GetPoint();
if (proof.Cy.Length != _attrPoints.Count) return false;
CredentialPrivateKey priv = keyPair.Private;
// Z = C_V - W - x0·C_x0 - x1·C_x1 - Σ y_i·C_y_i - y0·M0
Ristretto255 z = proof.Cv;
z = Ristretto255.Add(z, Ristretto255.Negate(priv.BigW));
z = Ristretto255.Add(z, Ristretto255.Negate(proof.Cx0.Multiply(priv.X0)));
z = Ristretto255.Add(z, Ristretto255.Negate(proof.Cx1.Multiply(priv.X1)));
for (int i = 0; i < proof.Cy.Length; i++)
z = Ristretto255.Add(z, Ristretto255.Negate(proof.Cy[i].Multiply(priv.Y[i])));
z = Ristretto255.Add(z, Ristretto255.Negate(_attrPoints[0].Multiply(priv.Y[0])));
Ristretto255 ii = keyPair.Public.IFor(_attrPoints.Count);
Dictionary<string, Ristretto255> points = PrepareNonAttrPoints(ii, proof.Cx0, proof.Cx1, proof.Cy);
foreach (AttrRef attr in _attrs)
{
points[$"C_y{attr.First}"] = proof.Cy[attr.First];
if (attr.KeyIndex is not null)
{
points[$"E_A{attr.First}"] = _attrPoints[attr.First];
points[$"-E_A{attr.First}"] = Ristretto255.Negate(_attrPoints[attr.First]);
points[$"C_y{attr.Second}-E_A{attr.Second}"] =
Ristretto255.Add(proof.Cy[attr.Second], Ristretto255.Negate(_attrPoints[attr.Second]));
}
else
{
z = Ristretto255.Add(z, Ristretto255.Negate(_attrPoints[attr.First].Multiply(priv.Y[attr.First])));
}
}
points["Z"] = z;
return PresentationProofBuilder.BuildStatement(_keys, _attrs).VerifyProof(proof.PokshoProof, points, _message);
}
private Dictionary<string, Ristretto255> PrepareNonAttrPoints(
Ristretto255 ii, Ristretto255 cx0, Ristretto255 cx1, Ristretto255[] cy)
{
var s = CredentialSystem.SystemParams.Hardcoded;
var p = new Dictionary<string, Ristretto255>
{
["I"] = ii, ["C_x0"] = cx0, ["C_x1"] = cx1, ["G_x0"] = s.GX0, ["G_x1"] = s.GX1,
};
if (_keys.Count > 0)
{
p["0"] = Ristretto255.Identity;
Ristretto255 sumA = Ristretto255.Identity;
bool any = false;
foreach (EncryptionKeyContext k in _keys)
{
if (k.PublicKeyA is { } a)
{
p[$"G_a1_{k.Id}"] = k.Ga1; p[$"G_a2_{k.Id}"] = k.Ga2;
sumA = Ristretto255.Add(sumA, a); any = true;
}
}
if (any) p["sum(A)"] = sumA;
}
string[] gy = { "G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6" };
for (int i = 0; i < _attrPoints.Count; i++) p[gy[i]] = s.GY[i];
p["C_y0"] = cy[0];
return p;
}
}