Add project files.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms).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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
Reference in New Issue
Block a user