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
+420
View File
@@ -0,0 +1,420 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Traits used for the attributes in a credential.
//!
//! Your attribute types must implement one of these traits. There are three kinds of supported
//! attributes:
//! - [`PublicAttribute`], which does not need to be hidden from the issuing server or verifying
//! server.
//! - [`Attribute`] (the reason for this entire credential system), which is hidden from the
//! verifying server using verifiable encryption, and may be hidden from the issuing server as
//! well with [blind issuance](crate::issuance::blind).
//! - [`RevealedAttribute`], which is hidden from the issuing server and then revealed to the
//! verifying server.
use std::marker::PhantomData;
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use derive_where::derive_where;
use partial_default::PartialDefault;
use poksho::ShoApi;
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use crate::VerificationFailure;
use crate::sho::ShoExt;
/// An attribute that doesn't need to be hidden from the issuing server or verifying server.
///
/// This can be encoded more efficiently, making for smaller, faster proofs.
/// (All public attributes get hashed together along with the credential type.)
pub trait PublicAttribute {
/// Mixes `self` into the hash computed by `sho`.
///
/// This will usually be implemented by calling [`ShoApi::absorb_and_ratchet`] one or more
/// times.
fn hash_into(&self, sho: &mut dyn ShoApi);
}
impl PublicAttribute for [u8] {
fn hash_into(&self, sho: &mut dyn ShoApi) {
sho.absorb_and_ratchet(self)
}
}
impl<const LEN: usize> PublicAttribute for [u8; LEN] {
fn hash_into(&self, sho: &mut dyn ShoApi) {
self.as_slice().hash_into(sho)
}
}
impl PublicAttribute for u32 {
fn hash_into(&self, sho: &mut dyn ShoApi) {
self.to_be_bytes().hash_into(sho)
}
}
impl PublicAttribute for u64 {
fn hash_into(&self, sho: &mut dyn ShoApi) {
self.to_be_bytes().hash_into(sho)
}
}
/// An attribute representable as a pair of [`RistrettoPoint`s](RistrettoPoint).
///
/// Used for credential attributes that need to take advantage of homomorphic encryption. Attributes
/// that never need to be hidden should use [`PublicAttribute`] instead. Attributes that only need
/// to be hidden during issuance may use the more compact [`RevealedAttribute`] instead.
///
/// For an attribute that is encrypted, both the attribute type and its corresponding ciphertext
/// type should conform to this trait. Note that blinded attributes do not conform to this trait, as
/// they have a different representation.
pub trait Attribute {
/// Converts `self` into a pair of points.
///
/// It is strongly recommended for an attribute's non-encrypted form that you generate the first
/// point by hashing and use the second to encode your encrypted information.
///
/// The encrypted attribute should apply [`KeyPair`]'s encryption to the point.
fn as_points(&self) -> [RistrettoPoint; 2];
}
impl Attribute for [RistrettoPoint; 2] {
fn as_points(&self) -> [RistrettoPoint; 2] {
*self
}
}
/// A domain for a [`KeyPair`].
///
/// This provides separation between different keys and ciphertexts, so that statically and
/// dynamically they can't get mixed up or substituted for one another.
///
/// # Example
///
/// ```
/// # use curve25519_dalek::RistrettoPoint;
/// # type UserId = [RistrettoPoint; 2];
/// struct UserIdEncryption;
/// impl zkcredential::attributes::Domain for UserIdEncryption {
/// type Attribute = UserId;
/// const ID: &'static str = "MyCompany_UserIdEncryption_20231011";
///
/// fn G_a() -> [RistrettoPoint; 2] {
/// static STORAGE: std::sync::OnceLock<[RistrettoPoint; 2]> = std::sync::OnceLock::new();
/// *zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
/// }
/// }
/// ```
pub trait Domain {
/// The attribute type used in this encryption domain.
type Attribute: Attribute;
/// A unique ID for this key (and its corresponding key pair)
///
/// This is used to identify and distinguish keys when constructing or validating a proof,
/// so make sure it's unique!
const ID: &'static str;
/// The "generator points" for this key
///
/// This can be a statically-chosen pair of points; it's used to construct the `A` point for a
/// [`PublicKey`].
///
/// A reasonable default implementation would use `derive_default_generator_points` with static
/// storage, for caching the resulting points:
///
/// ```
/// # use curve25519_dalek::RistrettoPoint;
/// # struct Example;
/// # impl zkcredential::attributes::Domain for Example {
/// # type Attribute = [RistrettoPoint; 2];
/// # const ID: &'static str = "20231030_Example";
/// fn G_a() -> [RistrettoPoint; 2] {
/// static STORAGE: std::sync::OnceLock<[RistrettoPoint; 2]> = std::sync::OnceLock::new();
/// *zkcredential::attributes::derive_default_generator_points::<Self>(&STORAGE)
/// }
/// # }
/// ```
///
/// Unfortunately this can't be provided as a default implementation, because that would result
/// in every domain sharing the same `STORAGE`, as if it were declared outside the trait.
fn G_a() -> [RistrettoPoint; 2];
}
/// Derives reasonable generator points `G_a` for `D`, based on its [`ID`][Domain::ID], and caches
/// them in `storage`.
pub fn derive_default_generator_points<D: Domain>(
storage: &std::sync::OnceLock<[RistrettoPoint; 2]>,
) -> &[RistrettoPoint; 2] {
fn derive_impl<D: Domain>() -> [RistrettoPoint; 2] {
let mut sho = poksho::ShoHmacSha256::new(b"Signal_ZKCredential_Domain_20231011");
sho.absorb_and_ratchet(D::ID.as_bytes());
let G_a1 = sho.get_point();
let G_a2 = sho.get_point();
[G_a1, G_a2]
}
let result = storage.get_or_init(derive_impl::<D>);
debug_assert!(
result == &derive_impl::<D>(),
"initialized with non-default points for {}",
D::ID,
);
result
}
/// A key used to encrypt attributes.
///
/// Using different keys for different attribute types prevents "type confusion", where two
/// attributes coincidentally have the same encoding as RistrettoPoints. The encryption may also
/// have other purposes, such as the encryption of UUIDs and profile keys in a Signal group, and
/// therefore being able to use existing keys is important.
///
/// The private key in this system is a pair of scalars `a1` and `a2`. Attributes are encrypted as
/// `E_A1 = a1 * M1; E_A2 = a2 * E_A1 + M2`.
///
/// Defined in Chase-Perrin-Zaverucha section 4.1.
///
/// See also [`PublicKey`].
#[derive(Serialize, Deserialize, PartialDefault)]
#[derive_where(Clone, Copy, Eq)]
#[partial_default(bound = "")]
#[non_exhaustive]
#[allow(missing_docs)]
pub struct KeyPair<D> {
pub a1: Scalar,
pub a2: Scalar,
#[serde(bound = "")]
pub public_key: PublicKey<D>,
}
impl<D> subtle::ConstantTimeEq for KeyPair<D> {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.a1.ct_eq(&other.a1) & self.a2.ct_eq(&other.a2)
}
}
impl<D> PartialEq for KeyPair<D> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
/// A key used to validate encrypted attributes.
///
/// Using different keys for different attribute types prevents "type confusion", where two
/// attributes coincidentally have the same encoding as RistrettoPoints. The encryption may also
/// have other purposes, such as the encryption of UUIDs and profile keys in a Signal group, and
/// therefore being able to use existing keys is important.
///
/// Defined in Chase-Perrin-Zaverucha section 4.1.
///
/// See also [`KeyPair`].
#[derive(Serialize, Deserialize, PartialDefault)]
#[derive_where(Clone, Copy, Eq)]
#[partial_default(bound = "")]
pub struct PublicKey<D> {
#[allow(missing_docs)]
pub A: RistrettoPoint,
#[serde(skip)]
domain: PhantomData<fn(D) -> D>,
}
impl<D> subtle::ConstantTimeEq for PublicKey<D> {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.A.ct_eq(&other.A)
}
}
impl<D> PartialEq for PublicKey<D> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl<D: Domain> KeyPair<D> {
/// Generates a new KeyPair from the hash state in `sho`.
///
/// Passing the same `sho` state in will produce the same key pair every time.
pub fn derive_from(sho: &mut dyn ShoApi) -> Self {
let a1 = sho.get_scalar();
let a2 = sho.get_scalar();
Self::from_scalars(a1, a2)
}
fn from_scalars(a1: Scalar, a2: Scalar) -> Self {
let [G_a1, G_a2] = D::G_a();
let A = a1 * G_a1 + a2 * G_a2;
Self {
a1,
a2,
public_key: PublicKey {
A,
domain: PhantomData,
},
}
}
/// Creates a KeyPair that's the inverse of `other`.
///
/// That is, if `k_inv` is `KeyPair::inverse_of(k)`, then `attr.as_points() ==
/// k_inv.encrypt(k.encrypt(&attr))`.
///
/// Note that the domain of `Self` doesn't have to be related to the domain of `other`. This can
/// be useful when the inverted key is used on derived values.
///
/// Don't use this to decrypt points; there are more efficient ways to do that. See
/// [`Self::decrypt_to_second_point`].
pub fn inverse_of<D2: Domain>(other: &KeyPair<D2>) -> Self {
assert_ne!(
D::ID,
D2::ID,
"You must provide a new domain for an inverse key"
);
let a1 = other.a1.invert();
let a2 = -(other.a1 * other.a2);
Self::from_scalars(a1, a2)
}
/// Encrypts `attr` according to Chase-Perrin-Zaverucha section 4.1.
#[inline]
pub fn encrypt(&self, attr: &D::Attribute) -> Ciphertext<D> {
self.encrypt_arbitrary_attribute(attr)
}
/// Encrypts `attr` according to Chase-Perrin-Zaverucha section 4.1, even if the attribute is
/// not normally associated with this key.
///
/// Allows controlling the domain of the resulting ciphertext, to not get confused with the
/// usual ciphertexts produced by [`Self::encrypt`].
#[inline]
pub fn encrypt_arbitrary_attribute<D2>(&self, attr: &dyn Attribute) -> Ciphertext<D2> {
let [M1, M2] = attr.as_points();
let E_A1 = self.a1 * M1;
let E_A2 = (self.a2 * E_A1) + M2;
Ciphertext {
E_A1,
E_A2,
domain: PhantomData,
}
}
/// Returns the second point from the plaintext that produced `ciphertext`
///
/// The encryption form allows recovering M2 from the ciphertext as `M2 = E_A2 - a2 * E_A1`. For
/// certain attributes, this may be enough to recover the value, making this a reversible
/// encryption system. However, it is **critical** to check that the decoded value produces the
/// same `E_A1` when re-encrypted:
///
/// ```ignored
/// a1 * HashToPoint(DecodeFromPoint(M2)) == E_A1
/// ```
///
/// This addresses the fact that this method is otherwise "garbage in, garbage out": it will
/// "decrypt" *any* ciphertext passed to it regardless of whether or not that ciphertext came
/// from a valid plaintext, encrypted using the same key.
///
/// Produces an error if `E_A1` is the Ristretto basepoint, which would imply that `a1` is not
/// actually encrypting anything.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
pub fn decrypt_to_second_point(
&self,
ciphertext: &Ciphertext<D>,
) -> Result<RistrettoPoint, VerificationFailure> {
if ciphertext.E_A1 == RISTRETTO_BASEPOINT_POINT {
return Err(VerificationFailure);
}
Ok(ciphertext.E_A2 - self.a2 * ciphertext.E_A1)
}
}
/// An attribute encrypted with [`KeyPair::encrypt`].
#[derive(Serialize, Deserialize, PartialDefault)]
#[derive_where(Clone, Copy, Eq)]
#[partial_default(bound = "")]
pub struct Ciphertext<D> {
E_A1: RistrettoPoint,
E_A2: RistrettoPoint,
#[serde(skip)]
domain: PhantomData<fn(D) -> D>,
}
impl<D> subtle::ConstantTimeEq for Ciphertext<D> {
fn ct_eq(&self, other: &Self) -> subtle::Choice {
self.E_A1.ct_eq(&other.E_A1) & self.E_A2.ct_eq(&other.E_A2)
}
}
impl<D> PartialEq for Ciphertext<D> {
fn eq(&self, other: &Self) -> bool {
self.ct_eq(other).into()
}
}
impl<D> Attribute for Ciphertext<D> {
#[inline]
fn as_points(&self) -> [RistrettoPoint; 2] {
[self.E_A1, self.E_A2]
}
}
/// An attribute that is [blinded](crate::issuance::blind) to the issuing server but revealed to the
/// verifying server.
///
/// Used only in the very specific case described above. Attributes that never need to be hidden
/// should use [`PublicAttribute`] instead; attributes that need to be hidden from the verifying
/// server should use the standard [`Attribute`].
///
/// This scenario does not appear in the Chase-Perrin-Zaverucha paper, but is a simplified version
/// of the blind issuance protocol shown in section 5.9.
pub trait RevealedAttribute {
/// Converts `self` to a point.
///
/// It is strongly recommended you do this by hashing unless you have a specific reason to do
/// otherwise.
fn as_point(&self) -> RistrettoPoint;
}
impl RevealedAttribute for RistrettoPoint {
fn as_point(&self) -> RistrettoPoint {
*self
}
}
#[cfg(test)]
mod tests {
use std::sync::OnceLock;
use super::*;
struct ExampleDomain;
impl Domain for ExampleDomain {
type Attribute = [RistrettoPoint; 2];
const ID: &'static str = "TestDomain";
fn G_a() -> [RistrettoPoint; 2] {
static STORAGE: OnceLock<[RistrettoPoint; 2]> = OnceLock::new();
*derive_default_generator_points::<Self>(&STORAGE)
}
}
#[test]
fn derive_default_generator_points_works() {
let _ = ExampleDomain::G_a();
}
#[test]
#[should_panic]
#[cfg(debug_assertions)]
fn derive_default_generator_points_checks_for_reuse_in_debug_builds() {
let storage = std::sync::OnceLock::from([RistrettoPoint::default(); 2]);
derive_default_generator_points::<ExampleDomain>(&storage);
}
}
+262
View File
@@ -0,0 +1,262 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Types used in both the issuance and presentation of credentials
use std::sync::LazyLock;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use partial_default::PartialDefault;
use poksho::{ShoApi, ShoHmacSha256, ShoSha256};
use serde::{Deserialize, Serialize};
use crate::RANDOMNESS_LEN;
use crate::sho::ShoExt;
/// A credential created by the issuing server over a set of attributes.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
// This type intentionally does not implement `Copy` to make it harder to
// accidentally duplicate these values.
pub struct Credential {
pub(crate) t: Scalar,
pub(crate) U: RistrettoPoint,
pub(crate) V: RistrettoPoint,
}
/// A secret key used to compute a MAC over a set of attributes
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Serialize, Deserialize, Clone, PartialDefault)]
pub(crate) struct CredentialPrivateKey {
pub(crate) w: Scalar,
pub(crate) wprime: Scalar,
pub(crate) W: RistrettoPoint,
pub(crate) x0: Scalar,
pub(crate) x1: Scalar,
pub(crate) y: [Scalar; NUM_SUPPORTED_ATTRS],
}
impl CredentialPrivateKey {
/// Creates a new secret key using the given source of random bytes.
fn generate(randomness: [u8; RANDOMNESS_LEN]) -> Self {
let mut sho =
ShoHmacSha256::new(b"Signal_ZKCredential_CredentialPrivateKey_generate_20230410");
sho.absorb_and_ratchet(&randomness);
let system = *SYSTEM_PARAMS;
let w = sho.get_scalar();
let W = w * system.G_w;
let wprime = sho.get_scalar();
let x0 = sho.get_scalar();
let x1 = sho.get_scalar();
let y = [(); NUM_SUPPORTED_ATTRS].map(|_| sho.get_scalar());
Self {
w,
wprime,
W,
x0,
x1,
y,
}
}
/// Produces a MAC over the given attributes.
///
/// Implements the credential computation described in Chase-Perrin-Zaverucha section 3.1.
///
/// # Panics
/// if more than [`NUM_SUPPORTED_ATTRS`] attributes are passed in.
pub(crate) fn credential_core(&self, M: &[RistrettoPoint], sho: &mut dyn ShoApi) -> Credential {
assert!(
M.len() <= NUM_SUPPORTED_ATTRS,
"more than {NUM_SUPPORTED_ATTRS} attributes not supported"
);
let t = sho.get_scalar();
let U = sho.get_point();
let mut V = self.W + (self.x0 + self.x1 * t) * U;
for (yn, Mn) in self.y.iter().zip(M) {
V += yn * Mn;
}
Credential { t, U, V }
}
}
/// A public key used by the client to receive and verify credentials.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Serialize, Deserialize, Clone, PartialDefault)]
pub struct CredentialPublicKey {
pub(crate) C_W: RistrettoPoint,
/// The value of `I` depends on the total number of attributes used.
///
/// In the original paper, `I` is computed over the maximum number of attributes only, but that
/// makes presentation proofs larger for credentials that don't use that many attributes. Here
/// we provide `I_n` for any supported number of attributes. We do skip `I_0`, since that would
/// be a credential with only public attributes, in which case you could just use a classic MAC.
I: [RistrettoPoint; NUM_SUPPORTED_ATTRS - 1],
}
impl CredentialPublicKey {
pub(crate) fn I(&self, num_attrs: usize) -> RistrettoPoint {
// `- 1` because we would normally want the third entry in the list for a three-attribute
// credential (the usual conversion from one-based counts to zero-based indexes).
// `- 1` again because we skip `I_0`; a one-attribute credential would only have public
// attributes.
self.I[num_attrs - 2]
}
}
impl<'a> From<&'a CredentialPrivateKey> for CredentialPublicKey {
fn from(private_key: &'a CredentialPrivateKey) -> Self {
let system = *SYSTEM_PARAMS;
let C_W = private_key.W + (private_key.wprime * system.G_wprime);
let mut I_i = system.G_V - (private_key.x0 * system.G_x0) - (private_key.x1 * system.G_x1);
let mut y_and_G_y_iter = private_key.y.iter().zip(system.G_y);
let (y0, G_y0) = y_and_G_y_iter.next().expect("correct number of parameters");
I_i -= y0 * G_y0;
let I = [(); NUM_SUPPORTED_ATTRS - 1].map(|_| {
let (yn, G_yn) = y_and_G_y_iter.next().expect("correct number of parameters");
I_i -= yn * G_yn;
I_i
});
debug_assert!(y_and_G_y_iter.next().is_none());
CredentialPublicKey { C_W, I }
}
}
/// A key pair used by the issuing server to sign credentials.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Deserialize, Clone, PartialDefault)]
#[serde(from = "CredentialPrivateKey")]
pub struct CredentialKeyPair {
private_key: CredentialPrivateKey,
public_key: CredentialPublicKey,
}
impl CredentialKeyPair {
/// Generates a new key pair.
pub fn generate(randomness: [u8; RANDOMNESS_LEN]) -> Self {
CredentialPrivateKey::generate(randomness).into()
}
pub(crate) fn private_key(&self) -> &CredentialPrivateKey {
&self.private_key
}
/// Gets the public key.
pub fn public_key(&self) -> &CredentialPublicKey {
&self.public_key
}
}
impl From<CredentialPrivateKey> for CredentialKeyPair {
fn from(private_key: CredentialPrivateKey) -> Self {
let public_key = CredentialPublicKey::from(&private_key);
Self {
private_key,
public_key,
}
}
}
impl Serialize for CredentialKeyPair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.private_key.serialize(serializer)
}
}
static SYSTEM_PARAMS: LazyLock<SystemParams> = LazyLock::new(SystemParams::generate);
pub(crate) const NUM_SUPPORTED_ATTRS: usize = 7; // 1 aggregate public, 3 two-point private
/// Parameters shared by the client and server.
///
/// User code never needs to explicitly reference these.
///
/// Defined in Chase-Perrin-Zaverucha section 3.1.
#[derive(Copy, Clone, Serialize, Deserialize)]
pub(crate) struct SystemParams {
pub(crate) G_w: RistrettoPoint,
pub(crate) G_wprime: RistrettoPoint,
pub(crate) G_x0: RistrettoPoint,
pub(crate) G_x1: RistrettoPoint,
pub(crate) G_V: RistrettoPoint,
pub(crate) G_z: RistrettoPoint,
pub(crate) G_y: [RistrettoPoint; NUM_SUPPORTED_ATTRS],
}
impl SystemParams {
/// An arbitrary set of independent points generated through a constant sequence of hash
/// operations.
fn generate() -> Self {
let mut sho = ShoSha256::new(b"Signal_ZKCredential_ConstantSystemParams_generate_20230410");
let G_w = sho.get_point();
let G_wprime = sho.get_point();
let G_x0 = sho.get_point();
let G_x1 = sho.get_point();
let G_V = sho.get_point();
let G_z = sho.get_point();
let G_y = [(); NUM_SUPPORTED_ATTRS].map(|_| sho.get_point());
SystemParams {
G_w,
G_wprime,
G_x0,
G_x1,
G_V,
G_z,
G_y,
}
}
pub fn get_hardcoded() -> SystemParams {
*SYSTEM_PARAMS
}
}
#[cfg(test)]
mod tests {
use const_str::hex;
use super::*;
impl SystemParams {
const SYSTEM_HARDCODED: &'static [u8] = &hex!(
"589c8718e8263a53a78932b6212a46e7fd52de3ad157b5bb277dba494cfd3471d4cc5f90685952917b33366efcce0512a1f8d70f974758266cb04fc424346d37b20f49cb2a081c94b1771fd8c172ae21785c61ea2c7e31947ce351e7b5ff07028c5329beb87b317ffcd981e440819d91136c988d6d9fbea4a87e55ed24a5993aa02f688ab1d3bd19056f94c8a44b8faddfa3c9c79c95ad44311a7bf00e5e862ec2c399f0d689dfb8c2dc0d7caba32afcf58cf0d85f78195a0b5ab732f565595492cfd982321d1f9be4b21fe6a0214306023d6a05d0d23f67ddc1c0400e5e0a5e92d17595131b7a095e740b884b8c9bb0226a39cfd027c769c4f4677c51f21b24da81fb2bd1356a9d0650f6a63fcc90d93bd74a954ba6f75f0e9fca47a6d21734bce7b28f06b76ef2c44d20a07026534e586eb8e1038874a93e44de362ce7bc0844bffc88e390c62519e281aa6fd53ff9ddd1d9ba303cf70004278ea2ae66ce05a2749d29eba56f3efe99e42902825c473dfc3c154c3762d2e76bd103f629d250b2d9d5c243a4cf8f3be21a84f153f44e2733a105cf780a20f03d84fe1ebbeb0e"
);
}
#[test]
fn test_system() {
let params = SystemParams::generate();
let serialized = bincode::serialize(&params).expect("can serialize");
println!("PARAMS = {serialized:#x?}");
assert!(serialized == SystemParams::SYSTEM_HARDCODED);
}
#[test]
fn round_trip_key_pair() {
let key_pair = CredentialKeyPair::generate([0x42; RANDOMNESS_LEN]);
let serialized = bincode::serialize(&key_pair).unwrap();
let deserialized: CredentialKeyPair = bincode::deserialize(&serialized).unwrap();
assert_eq!(&key_pair.public_key.C_W, &deserialized.public_key.C_W);
assert_eq!(&key_pair.private_key.w, &deserialized.private_key.w);
}
}
+285
View File
@@ -0,0 +1,285 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! The generation and verification of credential issuance proofs.
//!
//! When the issuing server issues a credential, it also generates a proof that the credential
//! covers the correct attributes. The client receives the proof and credential together, verifies
//! the proof, and extracts the credential. By providing the same attributes in the same order, the
//! generation and verification procedures have parallel invocations. The size of the proof scales
//! linearly with the number of attributes.
//!
//! Credential issuance is defined in Chase-Perrin-Zaverucha section 3.2.
pub mod blind;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::traits::Identity;
use partial_default::PartialDefault;
use poksho::shoapi::ShoApiExt as _;
use poksho::{ShoApi, ShoHmacSha256};
use serde::{Deserialize, Serialize};
use crate::attributes::{Attribute, PublicAttribute};
use crate::credentials::{
Credential, CredentialKeyPair, CredentialPublicKey, NUM_SUPPORTED_ATTRS, SystemParams,
};
use crate::sho::ShoExt;
use crate::{RANDOMNESS_LEN, VerificationFailure};
/// Contains a [`Credential`] along with a proof of its validity.
///
/// Use [`IssuanceProofBuilder`] to validate and extract the credential.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct IssuanceProof {
credential: Credential,
poksho_proof: Vec<u8>,
}
/// Used to generate and verify issuance proofs.
///
/// The same type is used for both generation and verification; the issuing server will end by
/// calling [`issue`](Self::issue) and the client by calling [`verify`](Self::verify).
pub struct IssuanceProofBuilder<'a> {
public_attrs: ShoHmacSha256,
/// Directly accessed by [`blind::BlindedIssuanceProofBuilder`].
attr_points: Vec<RistrettoPoint>,
authenticated_message: &'a [u8],
}
impl<'a> IssuanceProofBuilder<'a> {
/// Initializes a new proof builder.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
pub fn new(label: &[u8]) -> Self {
Self::with_authenticated_message(label, &[])
}
/// Initializes the proof builder with a message that must match between the issuing server and
/// the client.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
/// `message`, however, is not an attribute and will not be part of the resulting credential; it
/// is merely part of the proof. This could, for example, be used to distinguish multiple proofs
/// that produce the same kind of credential.
pub fn with_authenticated_message(label: &[u8], message: &'a [u8]) -> Self {
Self {
public_attrs: ShoHmacSha256::new(label),
// Reserve the first point for public attributes
attr_points: vec![RistrettoPoint::identity()],
authenticated_message: message,
}
}
/// Adds a public attribute to the credential.
///
/// This is order-sensitive.
pub fn add_public_attribute(mut self, attr: &dyn PublicAttribute) -> Self {
attr.hash_into(&mut self.public_attrs);
self.public_attrs.ratchet();
self
}
/// Adds an attribute to the credential.
///
/// This is order-sensitive.
pub fn add_attribute(mut self, attr: &dyn Attribute) -> Self {
self.attr_points.extend(attr.as_points());
assert!(
self.attr_points.len() <= NUM_SUPPORTED_ATTRS,
"more than {} hidden attribute points not supported",
NUM_SUPPORTED_ATTRS - 1
);
self
}
fn get_poksho_statement(&self) -> poksho::Statement {
// See Chase-Perrin-Zaverucha section 3.2.
let mut st = poksho::Statement::new();
st.add("C_W", &[("w", "G_w"), ("wprime", "G_wprime")]);
// G_V - I = x0 * G_x0 + x1 * G_x1 + sum(yi * G_yi, i = 0..n)
let G_V_minus_I_terms: [_; NUM_SUPPORTED_ATTRS + 2] = [
("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", &G_V_minus_I_terms[..2 + self.attr_points.len()]);
// V = w * G_w + x0 * U + x1 * tU + sum(yi * Mi, i = 0..n)
let V_terms: [_; NUM_SUPPORTED_ATTRS + 3] = [
("w", "G_w"),
("x0", "U"),
("x1", "tU"),
("y0", "M0"),
("y1", "M1"),
("y2", "M2"),
("y3", "M3"),
("y4", "M4"),
("y5", "M5"),
("y6", "M6"),
];
st.add("V", &V_terms[..3 + self.attr_points.len()]);
st
}
fn finalize_public_attrs(&mut self) {
debug_assert!(self.attr_points[0] == RistrettoPoint::identity());
self.attr_points[0] = self.public_attrs.get_point();
}
/// Generates a [`poksho::PointArgs`] to be used in the final proof.
///
/// `total_attr_count` is passed in for [blind issuance](blind::BlindedIssuanceProofBuilder), in
/// which case the caller may provide additional attributes.
fn prepare_scalar_args(
&self,
key_pair: &CredentialKeyPair,
total_attr_count: usize,
) -> poksho::ScalarArgs {
assert!(
total_attr_count <= NUM_SUPPORTED_ATTRS,
"should have been enforced by the caller"
);
let mut scalar_args = poksho::ScalarArgs::new();
scalar_args.add("w", key_pair.private_key().w);
scalar_args.add("wprime", key_pair.private_key().wprime);
scalar_args.add("x0", key_pair.private_key().x0);
scalar_args.add("x1", key_pair.private_key().x1);
let y_names: [_; NUM_SUPPORTED_ATTRS] = ["y0", "y1", "y2", "y3", "y4", "y5", "y6"];
for (name, value) in y_names
.into_iter()
.take(total_attr_count)
.zip(key_pair.private_key().y.iter())
{
scalar_args.add(name, *value);
}
scalar_args
}
/// Generates a [`poksho::PointArgs`] to be used in the final proof.
///
/// The `credential` argument may be `None` when used for [blind
/// issuance](blind::BlindedIssuanceProofBuilder), in which case the caller is responsible for
/// adding its own points representing the credential.
fn prepare_point_args(
&self,
key: &CredentialPublicKey,
total_attr_count: usize,
credential: Option<&Credential>,
) -> poksho::PointArgs {
let system = SystemParams::get_hardcoded();
assert!(
total_attr_count <= NUM_SUPPORTED_ATTRS,
"should have been enforced by the caller"
);
let mut point_args = poksho::PointArgs::new();
point_args.add("C_W", key.C_W);
point_args.add("G_w", system.G_w);
point_args.add("G_wprime", system.G_wprime);
point_args.add("G_V-I", system.G_V - key.I(total_attr_count));
point_args.add("G_x0", system.G_x0);
point_args.add("G_x1", system.G_x1);
let G_y_names: [_; NUM_SUPPORTED_ATTRS] =
["G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6"];
for (name, value) in G_y_names
.into_iter()
.take(total_attr_count)
.zip(system.G_y.iter())
{
point_args.add(name, *value);
}
if let Some(credential) = credential {
point_args.add("V", credential.V);
point_args.add("U", credential.U);
point_args.add("tU", credential.t * credential.U);
}
let M_names: [_; NUM_SUPPORTED_ATTRS] = ["M0", "M1", "M2", "M3", "M4", "M5", "M6"];
for (name, value) in M_names.into_iter().zip(&self.attr_points) {
point_args.add(name, *value);
}
point_args
}
/// Issues a new credential over the accumulated attributes using the given `key_pair`.
///
/// `randomness` ensures several important properties:
/// - The generated credential is randomized (non-deterministic).
/// - The issuance proof uses a random nonce.
///
/// It is critical that different randomness is used each time a credential is issued. Failing
/// to do so effectively reveals the server's private key.
pub fn issue(
mut self,
key_pair: &CredentialKeyPair,
randomness: [u8; RANDOMNESS_LEN],
) -> IssuanceProof {
self.finalize_public_attrs();
let mut sho = ShoHmacSha256::new(b"Signal_ZKCredential_Issuance_20230410");
sho.absorb_and_ratchet(&randomness);
let credential = key_pair
.private_key()
.credential_core(&self.attr_points, &mut sho);
let scalar_args = self.prepare_scalar_args(key_pair, self.attr_points.len());
let point_args = self.prepare_point_args(
key_pair.public_key(),
self.attr_points.len(),
Some(&credential),
);
let poksho_proof = self
.get_poksho_statement()
.prove(
&scalar_args,
&point_args,
self.authenticated_message,
&sho.squeeze_and_ratchet_as_array::<RANDOMNESS_LEN>(),
)
.expect("valid proof");
IssuanceProof {
poksho_proof,
credential,
}
}
/// Verifies the given `proof` over the accrued attributes using the given `public_key`.
///
/// On successful verification, returns the [`Credential`] that was just proven valid.
pub fn verify(
mut self,
public_key: &CredentialPublicKey,
// Even though it would work with a borrow, this deliberately consumes
// IssuanceProof to indicate that you should not keep it around after
// you have extracted the credential.
proof: IssuanceProof,
) -> Result<Credential, VerificationFailure> {
self.finalize_public_attrs();
let point_args =
self.prepare_point_args(public_key, self.attr_points.len(), Some(&proof.credential));
match self.get_poksho_statement().verify_proof(
&proof.poksho_proof,
&point_args,
self.authenticated_message,
) {
Err(_) => Err(VerificationFailure),
Ok(_) => Ok(proof.credential),
}
}
}
+86
View File
@@ -0,0 +1,86 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Used to build custom _attribute-based anonymous credentials_ ("ABCs") and their associated
//! proofs.
//!
//! This crate's cryptographic abstractions are largely built around the idea of a _client,_ an
//! _issuing server,_ and a _verifying server._ The client sends a _credential request_ to the
//! issuing server, which returns a _credential_ in a _response._ The client validates the
//! credential and checks that the server has not fingerprinted it in any way, then generates a
//! _presentation_ for the credential and presents that to the verifying server when making a
//! request (to perform some operation). The verifying server validates that presentation and
//! performs that operation.
//!
//! Presentations should never be reused for multiple operations; that would allow the verifying
//! server to identify that the same user is responsible. Instead, a new presentation should be
//! generated from a cached credential for each operation.
//!
//! What's in a credential? It's essentially a MAC over several _attributes,_ such as the client's
//! account identifier. What's important is that the attributes in a credential support homomorphic
//! encryption, allowing the verifying server to validate attributes without the client revealing
//! them. A credential can even be issued over a _blinded attribute_ hidden from the issuing server,
//! matching whatever value the client has _committed_ to.
//!
//! In this model, the issuing and verifying servers share their private keys, but may otherwise be
//! independent; for Signal, the _issuing server_ is usually the main chat server (which knows who
//! the client is), and the _verifying server_ is the "storage service" where groups are managed
//! (which must not). However, it would be valid to have the same server perform both operations, as
//! long as the second connection can't be correlated with the first.
//!
//! This model is based on "[The Signal Private Group System and Anonymous Credentials Supporting
//! Efficient Verifiable Encryption][paper]", by Chase, Perrin, and Zaverucha.
//!
//! [paper]: https://eprint.iacr.org/2019/1416
#![allow(non_snake_case)]
#![warn(missing_docs, clippy::unwrap_used)]
/// A zkcredential operation failed to verify.
#[derive(Debug, thiserror::Error, displaydoc::Display)]
pub struct VerificationFailure;
/// A reasonable size of entropy to request for operations.
///
/// zkcredential uses explicit arrays of randomness rather than taking random number generators as
/// arguments because it makes it easier to write expected-output tests in the languages libsignal
/// is bridged to, which can't easily substitute a custom Rng.
pub const RANDOMNESS_LEN: usize = 32;
pub mod attributes;
pub mod credentials;
pub mod endorsements;
pub mod issuance;
pub mod presentation;
pub mod sho;
/// Helper type for implementing [`std::fmt::Debug`].
///
/// The `Debug::fmt` implementation for this type prints the wrapped value as
/// hex bytes.
pub struct PrintAsHex<T>(pub T);
impl std::fmt::Debug for PrintAsHex<&[u8]> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for b in self.0 {
write!(f, "{b:0x}")?
}
Ok(())
}
}
impl std::fmt::Debug for PrintAsHex<&curve25519_dalek::ristretto::CompressedRistretto> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
PrintAsHex(self.0.as_bytes().as_slice()).fmt(f)
}
}
impl std::fmt::Debug for PrintAsHex<&[curve25519_dalek::ristretto::CompressedRistretto]> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list()
.entries(self.0.iter().map(PrintAsHex))
.finish()
}
}
+711
View File
@@ -0,0 +1,711 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! The generation and verification of credential presentation proofs
//!
//! When the client wishes to use a credential, it generates a _presentation proof_ over the same
//! attributes that went into the original credential. This allows the client to demonstrate that
//! they hold a credential over certain attributes without actually revealing those attributes. The
//! verifying server will verify the proof against the encrypted forms of those attributes and is
//! thus assured that the client does hold a credential from the issuing server.
//!
//! By providing the same attributes in the same order, a proof can be generated and verified with
//! parallel invocations. The size of the proof scales linearly with the number of attributes.
//!
//! It is recommended that the client generate a new presentation for every use of their private
//! credential, so that the verifying server cannot track repeated uses of the same presentation. Of
//! course, the encrypted forms of the attributes might also allow the verifying server to correlate
//! requests over time.
//!
//! Credential presentation is defined in Chase-Perrin-Zaverucha section 3.2; proofs for verifiable
//! encryption are defined in section 4.1.
use curve25519_dalek::Scalar;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::traits::Identity;
use partial_default::PartialDefault;
use poksho::shoapi::ShoApiExt as _;
use poksho::{ShoApi, ShoHmacSha256};
use serde::{Deserialize, Serialize};
use crate::attributes::{self, Attribute, PublicAttribute, RevealedAttribute};
use crate::credentials::{
Credential, CredentialKeyPair, CredentialPrivateKey, CredentialPublicKey, NUM_SUPPORTED_ATTRS,
SystemParams,
};
use crate::sho::ShoExt;
use crate::{RANDOMNESS_LEN, VerificationFailure};
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
struct PresentationProofCommitments {
C_x0: RistrettoPoint,
C_x1: RistrettoPoint,
C_V: RistrettoPoint,
C_y: Vec<RistrettoPoint>,
}
/// Demonstrates to the _verifying server_ that the client holds a particular credential.
///
/// Use [`PresentationProofVerifier`] to validate the proof.
#[derive(Clone, Serialize, Deserialize, PartialDefault)]
pub struct PresentationProof {
commitments: PresentationProofCommitments,
poksho_proof: Vec<u8>,
}
struct AttributeRef {
key_index: Option<usize>,
first_point_index: usize,
second_point_index: usize,
}
/// A type-erased version of [`attributes::PublicKey`], to store heterogeneously in a
/// [`PresentationProofBuilderCore`].
struct AnyPublicKey {
id: &'static str,
G_a: fn() -> [RistrettoPoint; 2],
A: RistrettoPoint,
}
impl<D: attributes::Domain> From<attributes::PublicKey<D>> for AnyPublicKey {
fn from(value: attributes::PublicKey<D>) -> Self {
Self {
id: D::ID,
G_a: D::G_a,
A: value.A,
}
}
}
enum PublicKeyOrId {
PublicKey(AnyPublicKey),
Id(&'static str),
}
trait MayHavePublicKey {
fn id(&self) -> &'static str;
fn public_key(&self) -> Option<&AnyPublicKey>;
}
impl MayHavePublicKey for PublicKeyOrId {
fn id(&self) -> &'static str {
match self {
PublicKeyOrId::PublicKey(key) => key.id,
PublicKeyOrId::Id(id) => id,
}
}
fn public_key(&self) -> Option<&AnyPublicKey> {
if let PublicKeyOrId::PublicKey(public_key) = &self {
Some(public_key)
} else {
None
}
}
}
/// A type-erased version of [`attributes::KeyPair`], to store heterogeneously in a
/// [`PresentationProofBuilderCore`].
struct AnyKeyPair {
a1: Scalar,
a2: Scalar,
public_key_or_id: PublicKeyOrId,
}
impl AnyKeyPair {
fn without_public_key(self) -> Self {
Self {
public_key_or_id: PublicKeyOrId::Id(self.public_key_or_id.id()),
..self
}
}
}
impl<D: attributes::Domain> From<attributes::KeyPair<D>> for AnyKeyPair {
fn from(value: attributes::KeyPair<D>) -> Self {
Self {
a1: value.a1,
a2: value.a2,
public_key_or_id: PublicKeyOrId::PublicKey(value.public_key.into()),
}
}
}
impl MayHavePublicKey for AnyKeyPair {
fn id(&self) -> &'static str {
self.public_key_or_id.id()
}
fn public_key(&self) -> Option<&AnyPublicKey> {
self.public_key_or_id.public_key()
}
}
struct PresentationProofBuilderCore<'a, T: MayHavePublicKey> {
encryption_keys: Vec<T>,
attributes: Vec<AttributeRef>,
attr_points: Vec<RistrettoPoint>,
authenticated_message: &'a [u8],
}
/// Used to generate presentation proofs.
///
/// Public attributes are not included from the presentation proof; when the proof is verified, the
/// verifying server will provide its own copy of the public attributes to ensure that they haven't
/// been tampered with.
///
/// See also [`PresentationProofVerifier`].
pub struct PresentationProofBuilder<'a> {
core: PresentationProofBuilderCore<'a, AnyKeyPair>,
}
/// Used to verify presentation proofs.
///
/// By providing the same attributes in the same order, a proof can be generated and verified with
/// parallel invocations. The size of the proof scales linearly with the number of attributes.
///
/// Public attributes are not included from the presentation proof; when the proof is verified, the
/// verifying server will provide its own copy of the public attributes to ensure that they haven't
/// been tampered with, as mentioned in Chase-Perrin-Zaverucha section 3.2.
///
/// See also [`PresentationProofBuilder`].
pub struct PresentationProofVerifier<'a> {
core: PresentationProofBuilderCore<'a, PublicKeyOrId>,
public_attrs: ShoHmacSha256,
}
impl<'a, T: MayHavePublicKey> PresentationProofBuilderCore<'a, T> {
fn with_authenticated_message(message: &'a [u8]) -> Self {
Self {
encryption_keys: vec![],
attributes: vec![],
// Reserve the first point for public attributes
attr_points: vec![RistrettoPoint::identity()],
authenticated_message: message,
}
}
fn add_attribute(&mut self, attr_points: &[RistrettoPoint], key: Option<T>) {
let first_index = self.attr_points.len();
self.attr_points.extend(attr_points);
assert!(
self.attr_points.len() <= NUM_SUPPORTED_ATTRS,
"more than {} hidden attribute points not supported",
NUM_SUPPORTED_ATTRS - 1
);
let key_index = key.map(|key| {
let key_id = key.id();
match self
.encryption_keys
.iter()
.position(|key| key.id() == key_id)
{
Some(idx) => idx,
None => {
let idx = self.encryption_keys.len();
self.encryption_keys.push(key);
idx
}
}
});
// If we ever support attributes longer than two points we'll have to change this.
self.attributes.push(AttributeRef {
key_index,
first_point_index: first_index,
second_point_index: first_index + attr_points.len() - 1,
});
}
fn get_poksho_statement(&self) -> poksho::Statement {
let mut st = poksho::Statement::new();
// These terms are from Chase-Perrin-Zaverucha section 3.2.
st.add("Z", &[("z", "I")]);
st.add("C_x1", &[("t", "C_x0"), ("z0", "G_x0"), ("z", "G_x1")]);
// These terms are from Chase-Perrin-Zaverucha section 4.1,
// proving the validity of the encryption keys.
let mut encryption_sum_terms = vec![];
for key in &self.encryption_keys {
let key_id = key.id();
let a1 = format!("a1_{key_id}");
// These terms are an addition by Trevor Perrin to the original paper to more carefully
// ensure the validity of the encryption keys used.
// 0 = z1_uid * I + a1_uid * Z
st.add("0", &[(&format!("z1_{key_id}"), "I"), (&a1, "Z")]);
if key.public_key().is_some() {
encryption_sum_terms.push((a1, format!("G_a1_{key_id}")));
encryption_sum_terms.push((format!("a2_{key_id}"), format!("G_a2_{key_id}")));
}
}
if !encryption_sum_terms.is_empty() {
// sum(A) = (a1_uid * G_a1_uid) + (a2_uid * G_a2_uid) +
// (a1_profilekey * G_a1_profilekey) + (a2_profilekey * G_a2_profilekey) +
// ...
st.add(
"sum(A)",
&encryption_sum_terms
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect::<Vec<_>>(),
);
}
for attr in &self.attributes {
if let Some(key_index) = attr.key_index {
// If this attribute uses a key, it's a verifiably encrypted Attribute.
// These terms are from Chase-Perrin-Zaverucha section 4.1,
// proving that the ciphertext matches the attribute in the credential.
let key_id = self.encryption_keys[key_index].id();
// E_A1 = a1_uid * C_y1 + z1_uid * G_y1
st.add(
&format!("E_A{}", attr.first_point_index),
&[
(
&format!("a1_{key_id}"),
&format!("C_y{}", attr.first_point_index),
),
(
&format!("z1_{key_id}"),
&format!("G_y{}", attr.first_point_index),
),
],
);
// C_y2 - E_A2 = z * G_y2 + a2_uid * -E_A1
st.add(
&format!("C_y{0}-E_A{0}", attr.second_point_index),
&[
("z", &format!("G_y{}", attr.second_point_index)),
(
&format!("a2_{key_id}"),
&format!("-E_A{}", attr.first_point_index),
),
],
);
} else {
// If the attribute does not use a key, it's a RevealedAttribute.
// (We don't currently support hidden scalar attributes.)
// This is from section 3.2 again; C_y1 is otherwise unbound.
debug_assert_eq!(attr.first_point_index, attr.second_point_index);
// C_y1 = z * G_y1
st.add(
&format!("C_y{}", attr.first_point_index),
&[("z", &format!("G_y{}", attr.first_point_index))],
);
}
}
// Point 0 is a hardcoded public attribute.
st.add("C_y0", &[("z", "G_y0")]);
st
}
/// Generates [`poksho::PointArgs`] containing all points not derived from attributes.
///
/// This includes the credential key commitments `C_x0`, `C_x1`, and `C_y0`; the system points
/// `G_x0`, `G_x1`, and all `G_y{i}`; the appropriate issuing parameter point `I`; and the
/// points necessary to prove the validity of encryption keys: `0`, `G_a1_{key}`, `G_a2_{key}`,
/// and `sum(A)`.
///
/// The caller is responsible for handling the presenter's one-off public point `Z` (which the
/// verifier derives from the commitments and public attributes); the appropriate `C_y{i}` for
/// all attributes besides public attributes (depending on whether or not attributes are
/// encrypted); and the encryption-specific points `E_A{i}`, `-E_A{i}`, and `C_y{j}-E_A{j}`.
fn prepare_non_attribute_point_args(
&self,
I: RistrettoPoint,
commitments: &PresentationProofCommitments,
) -> poksho::PointArgs {
let credentials_system = SystemParams::get_hardcoded();
let mut point_args = poksho::PointArgs::new();
point_args.add("I", I);
point_args.add("C_x0", commitments.C_x0);
point_args.add("C_x1", commitments.C_x1);
point_args.add("G_x0", credentials_system.G_x0);
point_args.add("G_x1", credentials_system.G_x1);
if !self.encryption_keys.is_empty() {
point_args.add("0", RistrettoPoint::identity());
let mut sum_A = RistrettoPoint::identity();
for key in &self.encryption_keys {
if let Some(key) = key.public_key() {
let [G_a1, G_a2] = (key.G_a)();
point_args.add(format!("G_a1_{}", key.id), G_a1);
point_args.add(format!("G_a2_{}", key.id), G_a2);
sum_A += key.A;
}
}
if sum_A != RistrettoPoint::identity() {
point_args.add("sum(A)", sum_A);
}
}
let G_y_names: [_; NUM_SUPPORTED_ATTRS] =
["G_y0", "G_y1", "G_y2", "G_y3", "G_y4", "G_y5", "G_y6"];
for (G_y_name, G_yn) in G_y_names
.into_iter()
.take(self.attr_points.len())
.zip(credentials_system.G_y)
{
point_args.add(G_y_name, G_yn)
}
point_args.add("C_y0", commitments.C_y[0]);
// Other C_y depend on the form of the attribute.
point_args
}
}
impl<'a> PresentationProofBuilder<'a> {
/// Initializes a new proof builder.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential, but as
/// a public attribute it is ignored. It is merely here for symmetry with
/// [`PresentationProofVerifier::new`].
pub fn new(label: &[u8]) -> Self {
Self::with_authenticated_message(label, &[])
}
/// Initializes a new proof builder.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential, but as
/// a public attribute it is ignored. It is merely here for symmetry with
/// [`PresentationProofVerifier::with_authenticated_message`].
///
/// `message`, however, is not an attribute and is not part of the original credential; it is
/// merely part of the proof. This could, for example, be used to distinguish multiple proofs
/// that present the same kind of credential.
pub fn with_authenticated_message(label: &[u8], message: &'a [u8]) -> Self {
_ = label;
Self {
core: PresentationProofBuilderCore::with_authenticated_message(message),
}
}
/// Unnecessary: public attributes are passed directly to the verifying server.
#[deprecated = "Unnecessary: public attributes are passed directly to the verifying server."]
pub fn add_public_attribute(self, attr: &dyn PublicAttribute) -> Self {
_ = attr;
self
}
/// Adds an attribute to the proof, which will be encrypted using `key`.
///
/// This is order-sensitive.
pub fn add_attribute(
mut self,
attr: &dyn Attribute,
key: &attributes::KeyPair<impl attributes::Domain>,
) -> Self {
self.core
.add_attribute(&attr.as_points(), Some(AnyKeyPair::from(*key)));
self
}
/// Adds an attribute to the proof, which will be encrypted using `key`.
///
/// This still includes a proof that the attribute was correctly encrypted; however, the
/// verifying server will not be able to check which key performed that encryption.
///
/// This is order-sensitive.
pub fn add_attribute_without_verified_key(
mut self,
attr: &dyn Attribute,
key: &attributes::KeyPair<impl attributes::Domain>,
) -> Self {
self.core.add_attribute(
&attr.as_points(),
Some(AnyKeyPair::from(*key).without_public_key()),
);
self
}
/// Adds an attribute to check against the credential.
///
/// In practice `attr` is ignored in favor of letting the verifying server check the attribute
/// itself, but it's still necessary to call this method to indicate that there *is* an
/// attribute.
///
/// This is order-sensitive.
pub fn add_revealed_attribute(mut self, attr: &dyn RevealedAttribute) -> Self {
// We don't actually need the value! The server will check it for us.
_ = attr;
self.core.add_attribute(&[RistrettoPoint::identity()], None);
self
}
/// Generates the presentation of `credential` using the server-provided `public_key`.
///
/// Note that this does not consume `credential`; indeed, it is recommended to use a new
/// presentation every time you want to use a particular credential.
///
/// `randomness` ensures several important properties:
/// - The generated presentation is randomized (non-deterministic).
/// - The presentation proof uses a random nonce.
///
/// It is critical that different randomness is used each time a credential is issued. Failing
/// to do so allows different presentations to be linked to the same credential (and thus the
/// same user), and worse, effectively reveals any hidden Attributes and their encryption keys.
pub fn present(
self,
public_key: &CredentialPublicKey,
credential: &Credential,
randomness: [u8; RANDOMNESS_LEN],
) -> PresentationProof {
let credentials_system = SystemParams::get_hardcoded();
let mut sho = ShoHmacSha256::new(b"Signal_ZKCredential_Presentation_20230410");
sho.absorb_and_ratchet(&randomness);
let z = sho.get_scalar();
debug_assert!(
self.core.attr_points[0] == RistrettoPoint::identity(),
"public attributes are incorporated by the server"
);
// Note that Mn will be the identity element for both the first point and for any
// RevealedAttributes, so this will simply produce `z * G_yn` for those elements as in
// Chase-Perrin-Zaverucha section 3.2.
let C_y = credentials_system
.G_y
.iter()
.zip(&self.core.attr_points)
.map(|(G_yn, Mn)| z * G_yn + Mn)
.collect::<Vec<_>>();
let C_x0 = z * credentials_system.G_x0 + credential.U;
let C_V = z * credentials_system.G_V + credential.V;
let C_x1 = z * credentials_system.G_x1 + credential.t * credential.U;
let commitments = PresentationProofCommitments {
C_x0,
C_x1,
C_V,
C_y,
};
let z0 = -z * credential.t;
let I = public_key.I(self.core.attr_points.len());
let Z = z * I;
let mut scalar_args = poksho::ScalarArgs::new();
scalar_args.add("z", z);
scalar_args.add("t", credential.t);
scalar_args.add("z0", z0);
for key in &self.core.encryption_keys {
let key_id = key.id();
scalar_args.add(format!("a1_{key_id}"), key.a1);
scalar_args.add(format!("a2_{key_id}"), key.a2);
scalar_args.add(format!("z1_{key_id}"), -z * key.a1);
}
let mut point_args = self.core.prepare_non_attribute_point_args(I, &commitments);
point_args.add("Z", Z);
for attr in &self.core.attributes {
let &AttributeRef {
key_index,
first_point_index,
second_point_index,
} = attr;
point_args.add(
format!("C_y{first_point_index}"),
commitments.C_y[first_point_index],
);
if let Some(key_index) = key_index {
let key = &self.core.encryption_keys[key_index];
let E_A1 = key.a1 * self.core.attr_points[first_point_index];
let E_A2 = key.a2 * E_A1 + self.core.attr_points[second_point_index];
point_args.add(format!("E_A{first_point_index}"), E_A1);
point_args.add(format!("-E_A{first_point_index}"), -E_A1);
point_args.add(
format!("C_y{second_point_index}-E_A{second_point_index}"),
commitments.C_y[second_point_index] - E_A2,
);
} else {
debug_assert!(
self.core.attr_points[first_point_index] == RistrettoPoint::identity(),
"revealed attributes are incorporated by the server"
);
}
}
let poksho_proof = self
.core
.get_poksho_statement()
.prove(
&scalar_args,
&point_args,
self.core.authenticated_message,
&sho.squeeze_and_ratchet_as_array::<RANDOMNESS_LEN>(),
)
.expect("valid proof");
PresentationProof {
commitments,
poksho_proof,
}
}
}
impl<'a> PresentationProofVerifier<'a> {
/// Initializes a new proof verifier.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
pub fn new(label: &[u8]) -> Self {
Self::with_authenticated_message(label, &[])
}
/// Initializes a new proof verifier.
///
/// `label` is a mandatory public attribute that should uniquely identify the credential.
/// `message`, however, is not an attribute and is not part of the original credential; it is
/// merely part of the proof. This could, for example, be used to distinguish multiple proofs
/// that present the same kind of credential.
pub fn with_authenticated_message(label: &[u8], message: &'a [u8]) -> Self {
Self {
core: PresentationProofBuilderCore::with_authenticated_message(message),
public_attrs: ShoHmacSha256::new(label),
}
}
/// Adds a public attribute to check against the credential.
///
/// This is order-sensitive.
pub fn add_public_attribute(mut self, attr: &dyn PublicAttribute) -> Self {
attr.hash_into(&mut self.public_attrs);
self.public_attrs.ratchet();
self
}
/// Adds an encrypted attribute to check against the credential, along with the public key for
/// the key it was encrypted with.
///
/// This is order-sensitive.
pub fn add_attribute(
mut self,
attr: &dyn Attribute,
key: &attributes::PublicKey<impl attributes::Domain>,
) -> Self {
self.core.add_attribute(
&attr.as_points(),
Some(PublicKeyOrId::PublicKey(AnyPublicKey::from(*key))),
);
self
}
/// Adds an encrypted attribute to check against the credential, omitting the key it was
/// encrypted with.
///
/// This still checks that the attribute was correctly encrypted; it just can't enforce which
/// key did so.
///
/// This is order-sensitive.
pub fn add_attribute_without_verified_key(
mut self,
attr: &dyn Attribute,
key_id: &'static str,
) -> Self {
self.core
.add_attribute(&attr.as_points(), Some(PublicKeyOrId::Id(key_id)));
self
}
/// Adds an attribute to check against the credential, unencrypted.
///
/// This should only be used when the attribute is blinded from the issuing server, but visible
/// to the verifying server. Use public attributes when the value doesn't need to be hidden at
/// all.
///
/// This is order-sensitive.
pub fn add_revealed_attribute(mut self, attr: &dyn RevealedAttribute) -> Self {
self.core.add_attribute(&[attr.as_point()], None);
self
}
fn finalize_public_attrs(&mut self) {
debug_assert!(self.core.attr_points[0] == RistrettoPoint::identity());
self.core.attr_points[0] = self.public_attrs.get_point();
}
/// Verifies the given `proof` over the accrued attributes using the given `key_pair`.
pub fn verify(
mut self,
key_pair: &CredentialKeyPair,
proof: &PresentationProof,
) -> Result<(), VerificationFailure> {
self.finalize_public_attrs();
let PresentationProofCommitments {
C_x0,
C_x1,
C_V,
C_y,
} = &proof.commitments;
if C_y.len() != self.core.attr_points.len() {
return Err(VerificationFailure);
}
let CredentialPrivateKey { W, x0, x1, y, .. } = key_pair.private_key();
let mut Z = C_V - W - x0 * C_x0 - x1 * C_x1;
for (yn, C_yn) in y.iter().zip(C_y.iter()) {
Z -= yn * C_yn;
}
// Incorporate public attributes here so the server can check they haven't changed.
Z -= y[0] * self.core.attr_points[0];
let public_key = key_pair.public_key();
let I = public_key.I(self.core.attr_points.len());
let mut point_args = self
.core
.prepare_non_attribute_point_args(I, &proof.commitments);
for attr in &self.core.attributes {
let &AttributeRef {
first_point_index,
second_point_index,
key_index,
} = attr;
point_args.add(format!("C_y{first_point_index}"), C_y[first_point_index]);
if key_index.is_some() {
point_args.add(
format!("E_A{first_point_index}"),
self.core.attr_points[first_point_index],
);
point_args.add(
format!("-E_A{first_point_index}"),
-self.core.attr_points[first_point_index],
);
point_args.add(
format!("C_y{second_point_index}-E_A{second_point_index}"),
C_y[second_point_index] - self.core.attr_points[second_point_index],
);
} else {
// Check that the revealed attributes match the original issuance.
Z -= y[first_point_index] * self.core.attr_points[first_point_index];
}
}
point_args.add("Z", Z);
match self.core.get_poksho_statement().verify_proof(
&proof.poksho_proof,
&point_args,
self.core.authenticated_message,
) {
Err(_) => Err(VerificationFailure),
Ok(_) => Ok(()),
}
}
}
+29
View File
@@ -0,0 +1,29 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Additional utilities for poksho's [`ShoApi`] types.
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use poksho::ShoApi;
/// Extends [`ShoApi`] with convenience methods for generating Ristretto group elements.
pub trait ShoExt: ShoApi {
/// Uses [`ShoApi::squeeze_and_ratchet_into`] to generate a pseudorandom point.
fn get_point(&mut self) -> RistrettoPoint {
let mut point_bytes = [0u8; 64];
self.squeeze_and_ratchet_into(&mut point_bytes);
RistrettoPoint::from_uniform_bytes(&point_bytes)
}
/// Uses [`ShoApi::squeeze_and_ratchet_into`] to generate a pseudorandom scalar.
fn get_scalar(&mut self) -> Scalar {
let mut scalar_bytes = [0u8; 64];
self.squeeze_and_ratchet_into(&mut scalar_bytes);
Scalar::from_bytes_mod_order_wide(&scalar_bytes)
}
}
impl<T: ShoApi + ?Sized> ShoExt for T {}