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
+90
View File
@@ -0,0 +1,90 @@
//
// Copyright 2020-2022 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
pub const NUM_AUTH_CRED_ATTRIBUTES: usize = 3;
pub const NUM_PROFILE_KEY_CRED_ATTRIBUTES: usize = 4;
pub const NUM_RECEIPT_CRED_ATTRIBUTES: usize = 2;
pub const PRESENTATION_VERSION_1: u8 = 0;
pub const PRESENTATION_VERSION_2: u8 = 1;
pub const PRESENTATION_VERSION_3: u8 = 2;
pub const PRESENTATION_VERSION_4: u8 = 3;
pub const AES_KEY_LEN: usize = 32;
pub const AESGCM_NONCE_LEN: usize = 12;
pub const AESGCM_TAG_LEN: usize = 16;
pub const GROUP_MASTER_KEY_LEN: usize = 32;
pub const GROUP_SECRET_PARAMS_LEN: usize = 289;
pub const GROUP_PUBLIC_PARAMS_LEN: usize = 97;
pub const GROUP_IDENTIFIER_LEN: usize = 32;
pub const AUTH_CREDENTIAL_LEN: usize = 181;
pub const AUTH_CREDENTIAL_PRESENTATION_V2_LEN: usize = 461;
pub const AUTH_CREDENTIAL_RESPONSE_LEN: usize = 361;
pub const AUTH_CREDENTIAL_WITH_PNI_LEN: usize = 265;
pub const AUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN: usize = 425;
pub const PROFILE_KEY_LEN: usize = 32;
pub const PROFILE_KEY_CIPHERTEXT_LEN: usize = 65;
pub const PROFILE_KEY_COMMITMENT_LEN: usize = 97;
pub const EXPIRING_PROFILE_KEY_CREDENTIAL_LEN: usize = 153;
pub(crate) const PROFILE_KEY_CREDENTIAL_PRESENTATION_V1_LEN: usize = 713;
pub const PROFILE_KEY_CREDENTIAL_PRESENTATION_V2_LEN: usize = 713;
pub const PROFILE_KEY_CREDENTIAL_REQUEST_LEN: usize = 329;
pub const PROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN: usize = 473;
pub const EXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN: usize = 497;
pub const PROFILE_KEY_VERSION_LEN: usize = 32;
pub const PROFILE_KEY_VERSION_ENCODED_LEN: usize = 64;
pub const RECEIPT_CREDENTIAL_LEN: usize = 129;
pub const RECEIPT_CREDENTIAL_PRESENTATION_LEN: usize = 329;
pub const RECEIPT_CREDENTIAL_REQUEST_LEN: usize = 97;
pub const RECEIPT_CREDENTIAL_REQUEST_CONTEXT_LEN: usize = 177;
pub const RECEIPT_CREDENTIAL_RESPONSE_LEN: usize = 409;
pub const RECEIPT_SERIAL_LEN: usize = 16;
pub const RESERVED_LEN: usize = 1;
pub const SERVER_SECRET_PARAMS_LEN: usize = 2721;
pub const SERVER_PUBLIC_PARAMS_LEN: usize = 673;
pub const UUID_CIPHERTEXT_LEN: usize = 65;
pub const RANDOMNESS_LEN: usize = 32;
pub const SIGNATURE_LEN: usize = 64;
pub const UUID_LEN: usize = 16;
pub const ACCESS_KEY_LEN: usize = 16;
/// Seconds in a 24-hour cycle (ignoring leap seconds).
pub const SECONDS_PER_DAY: u64 = 86400;
pub const TEST_ARRAY_16: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
pub const TEST_ARRAY_16_1: [u8; 16] = [
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
];
pub const TEST_ARRAY_32: [u8; 32] = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31,
];
pub const TEST_ARRAY_32_1: [u8; 32] = [
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
];
pub const TEST_ARRAY_32_2: [u8; 32] = [
200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218,
219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231,
];
pub const TEST_ARRAY_32_3: [u8; 32] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
27, 28, 29, 30, 31, 32,
];
pub const TEST_ARRAY_32_4: [u8; 32] = [
2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33,
];
pub const TEST_ARRAY_32_5: [u8; 32] = [
3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34,
];
+166
View File
@@ -0,0 +1,166 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use bincode::Options;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use crate::ZkGroupDeserializationFailure;
fn zkgroup_bincode_options() -> impl bincode::Options {
bincode::DefaultOptions::new()
.with_fixint_encoding()
.with_little_endian()
.reject_trailing_bytes()
}
/// Deserializes a type using the standard zkgroup encoding (based on bincode).
///
/// The type must support [`PartialDefault`] to save on code size.
pub fn deserialize<'a, T: Deserialize<'a> + PartialDefault>(
bytes: &'a [u8],
) -> Result<T, ZkGroupDeserializationFailure> {
let mut result = T::partial_default();
// Use the same encoding options as plain bincode::deserialize, which we used historically,
// but also reject trailing bytes.
// See https://docs.rs/bincode/1.3.3/bincode/config/index.html#options-struct-vs-bincode-functions.
T::deserialize_in_place(
&mut bincode::Deserializer::from_slice(bytes, zkgroup_bincode_options()),
&mut result,
)
.map_err(|_| ZkGroupDeserializationFailure::new::<T>())?;
Ok(result)
}
/// Serializes a type using the standard zkgroup encoding (based on bincode).
pub fn serialize<T: Serialize>(value: &T) -> Vec<u8> {
zkgroup_bincode_options()
.serialize(value)
.expect("cannot fail")
}
/// Constant version number `C` as a type.
///
/// Zero-sized type that converts to and from for the value `C` via `Into`,
/// `TryFrom`, [`Serialize`], and [`Deserialize`]. Used for providing a version
/// tag at the beginning of serialized structs.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct VersionByte<const C: u8>;
impl<const C: u8> From<VersionByte<C>> for u8 {
fn from(VersionByte: VersionByte<C>) -> Self {
C
}
}
/// version byte was {found}, not {EXPECTED:?}
#[derive(Copy, Clone, Debug, Eq, PartialEq, displaydoc::Display)]
pub struct VersionMismatchError<const EXPECTED: u8> {
found: u8,
}
impl<const C: u8> TryFrom<u8> for VersionByte<C> {
type Error = VersionMismatchError<C>;
fn try_from(value: u8) -> Result<Self, Self::Error> {
(value == C)
.then_some(VersionByte::<C>)
.ok_or(VersionMismatchError::<C> { found: value })
}
}
impl<const C: u8> Serialize for VersionByte<C> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
u8::serialize(&C, serializer)
}
}
impl<'de, const C: u8> Deserialize<'de> for VersionByte<C> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = u8::deserialize(deserializer)?;
v.try_into().map_err(|_| {
<D::Error as serde::de::Error>::invalid_value(
serde::de::Unexpected::Unsigned(v.into()),
&format!("version `{C}`").as_str(),
)
})
}
}
/// Value that always serializes to and from `0u8`.
pub type ReservedByte = VersionByte<0>;
#[cfg(test)]
mod test {
use std::fmt::Debug;
use test_case::test_case;
use super::*;
#[derive(Debug, Serialize, Deserialize, PartialEq, PartialDefault)]
struct WithLeadingByte<T> {
leading: T,
string: String,
}
impl<T: Default> WithLeadingByte<T> {
fn test_value() -> Self {
Self {
leading: T::default(),
string: "a string".to_string(),
}
}
}
type WithReservedByte = WithLeadingByte<ReservedByte>;
type WithVersionByte = WithLeadingByte<VersionByte<42>>;
#[test_case(WithReservedByte::test_value(), 0)]
#[test_case(WithVersionByte::test_value(), 42)]
fn round_trip<T: Serialize + for<'a> Deserialize<'a> + PartialEq + PartialDefault + Debug>(
test_value: T,
expected_first_byte: u8,
) {
let serialized = crate::serialize(&test_value);
assert_eq!(serialized[0], expected_first_byte);
let deserialized: T = crate::deserialize(&serialized).expect("can deserialize");
assert_eq!(deserialized, test_value);
}
#[test_case(WithReservedByte::test_value())]
#[test_case(WithVersionByte::test_value())]
fn version_byte_wrong<
T: Serialize + for<'a> Deserialize<'a> + PartialEq + PartialDefault + Debug,
>(
test_value: T,
) {
let mut serialized = crate::serialize(&test_value);
// perturb the first byte.
serialized[0] += 1;
crate::deserialize::<T>(&serialized).expect_err("invalid version");
}
#[test]
fn version_byte_error_message() {
let mut bincode_serialized =
bincode::serialize(&WithVersionByte::test_value()).expect("should serialize");
bincode_serialized[0] = 41;
let error_message =
bincode::deserialize::<WithVersionByte>(&bincode_serialized).expect_err("should fail");
assert_eq!(
error_message.to_string(),
"invalid value: integer `41`, expected version `42`"
);
}
}
+63
View File
@@ -0,0 +1,63 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek_signal::ristretto::RistrettoPoint;
use curve25519_dalek_signal::scalar::Scalar;
use poksho::ShoApi;
use poksho::shoapi::ShoApiExt as _;
#[derive(Clone)]
pub struct Sho {
internal_sho: poksho::ShoHmacSha256,
}
impl Sho {
/// Creates a Sho and immediately absorbs `data` and ratchets.
///
/// This is exactly equivalent to calling [`Sho::new_seed`] followed by
/// [`Sho::absorb_and_ratchet`]. Meant for when you're not doing any other absorbing.
pub fn new(label: &[u8], data: &[u8]) -> Self {
let mut sho = Self::new_seed(label);
sho.absorb_and_ratchet(data);
sho
}
pub fn new_seed(label: &[u8]) -> Self {
let sho = poksho::ShoHmacSha256::new(label);
Sho { internal_sho: sho }
}
pub fn absorb_and_ratchet(&mut self, data: &[u8]) {
self.internal_sho.absorb_and_ratchet(data)
}
pub fn squeeze(&mut self, outlen: usize) -> Vec<u8> {
self.internal_sho.squeeze_and_ratchet(outlen)
}
pub fn squeeze_as_array<const N: usize>(&mut self) -> [u8; N] {
self.internal_sho.squeeze_and_ratchet_as_array()
}
pub fn get_point(&mut self) -> RistrettoPoint {
RistrettoPoint::from_uniform_bytes(&self.internal_sho.squeeze_and_ratchet_as_array())
}
pub fn get_point_single_elligator(&mut self) -> RistrettoPoint {
RistrettoPoint::from_uniform_bytes_single_elligator(
&self.internal_sho.squeeze_and_ratchet_as_array(),
)
}
pub fn get_scalar(&mut self) -> Scalar {
Scalar::from_bytes_mod_order_wide(&self.internal_sho.squeeze_and_ratchet_as_array())
}
}
impl AsMut<poksho::ShoHmacSha256> for Sho {
fn as_mut(&mut self) -> &mut poksho::ShoHmacSha256 {
&mut self.internal_sho
}
}
+147
View File
@@ -0,0 +1,147 @@
//
// Copyright 2020 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek_signal::scalar::Scalar;
use partial_default::PartialDefault;
use serde::{Deserialize, Serialize};
use zkcredential::attributes::PublicAttribute;
use crate::common::constants::*;
pub type AesKeyBytes = [u8; AES_KEY_LEN];
pub type GroupMasterKeyBytes = [u8; GROUP_MASTER_KEY_LEN];
pub type UidBytes = [u8; UUID_LEN];
pub type ProfileKeyBytes = [u8; PROFILE_KEY_LEN];
pub type RandomnessBytes = [u8; RANDOMNESS_LEN];
pub type SignatureBytes = [u8; SIGNATURE_LEN];
pub type NotarySignatureBytes = [u8; SIGNATURE_LEN];
pub type GroupIdentifierBytes = [u8; GROUP_IDENTIFIER_LEN];
pub type ProfileKeyVersionBytes = [u8; PROFILE_KEY_VERSION_LEN];
// TODO: Use ascii::Char when stable (the "encoding" is hex)
pub type ProfileKeyVersionEncodedBytes = [u8; PROFILE_KEY_VERSION_ENCODED_LEN];
// A random UUID that the receipt issuing server will blind authorize to redeem a given receipt
// level within a certain time frame.
pub type ReceiptSerialBytes = [u8; RECEIPT_SERIAL_LEN];
/// Timestamp measured in seconds past the epoch.
///
/// Clients should only accept round multiples of 86400 to avoid fingerprinting by the server.
/// For expirations, the timestamp should be within a couple of days into the future;
/// for redemption times, it should be within a day of the current date.
#[derive(
Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize, PartialDefault,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct Timestamp(u64);
impl Timestamp {
#[inline]
pub const fn from_epoch_seconds(seconds: u64) -> Self {
Self(seconds)
}
#[inline]
pub const fn epoch_seconds(&self) -> u64 {
self.0
}
#[inline]
pub const fn add_seconds(&self, seconds: u64) -> Self {
Self(self.0 + seconds)
}
#[inline]
pub const fn sub_seconds(&self, seconds: u64) -> Self {
Self(self.0 - seconds)
}
#[inline]
pub fn checked_add_seconds(&self, seconds: u64) -> Option<Self> {
self.0.checked_add(seconds).map(Self)
}
#[inline]
pub fn checked_sub_seconds(&self, seconds: u64) -> Option<Self> {
self.0.checked_sub(seconds).map(Self)
}
#[inline]
pub const fn is_day_aligned(&self) -> bool {
self.0 % SECONDS_PER_DAY == 0
}
#[inline]
pub fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
/// Number of seconds that `self` is after `before`.
///
/// Returns `0` if `self` is equal to or earlier than `before`.
pub(crate) fn saturating_seconds_since(&self, before: Timestamp) -> u64 {
self.0.saturating_sub(before.0)
}
}
impl From<Timestamp> for std::time::SystemTime {
fn from(Timestamp(seconds): Timestamp) -> Self {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(seconds)
}
}
impl From<std::time::SystemTime> for Timestamp {
fn from(timestamp: std::time::SystemTime) -> Self {
Self::from_epoch_seconds(
timestamp
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
)
}
}
impl rand::distr::Distribution<Timestamp> for rand::distr::StandardUniform {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> Timestamp {
Timestamp(Self::sample(self, rng))
}
}
impl PublicAttribute for Timestamp {
fn hash_into(&self, sho: &mut dyn poksho::ShoApi) {
self.0.hash_into(sho)
}
}
// Used to tell the server handling receipt redemptions what to redeem the receipt for. Clients
// should validate this matches their expectations.
pub type ReceiptLevel = u64;
pub fn encode_redemption_time(redemption_time: u32) -> Scalar {
let mut scalar_bytes: [u8; 32] = Default::default();
scalar_bytes[0..4].copy_from_slice(&redemption_time.to_be_bytes());
Scalar::from_bytes_mod_order(scalar_bytes)
}
pub fn encode_receipt_serial_bytes(receipt_serial_bytes: ReceiptSerialBytes) -> Scalar {
let mut scalar_bytes: [u8; 32] = Default::default();
scalar_bytes[0..16].copy_from_slice(&receipt_serial_bytes[..]);
Scalar::from_bytes_mod_order(scalar_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_scalar() {
let s_bytes = [0xFF; 32];
match bincode::deserialize::<Scalar>(&s_bytes) {
Err(_) => (),
Ok(_) => unreachable!(),
}
}
}