0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-19 19:42:19 +02:00

SVR - Clean up old protocol code.

This commit is contained in:
gram-signal 2024-08-30 17:03:15 -07:00 committed by GitHub
parent 1c2113617f
commit c65a5763df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1694 additions and 3210 deletions

View File

@ -164,17 +164,14 @@ impl From<attest::enclave::Error> for Error {
impl From<libsignal_svr3::Error> for Error {
fn from(err: libsignal_svr3::Error) -> Self {
use libsignal_svr3::{Error as LogicError, PPSSError};
use libsignal_svr3::Error as LogicError;
match err {
LogicError::Ppss(PPSSError::InvalidCommitment, tries_remaining)
| LogicError::RestoreFailed(tries_remaining) => Self::RestoreFailed(tries_remaining),
LogicError::RestoreFailed(tries_remaining) => Self::RestoreFailed(tries_remaining),
LogicError::BadResponseStatus(libsignal_svr3::ErrorStatus::Missing)
| LogicError::BadResponseStatus4(libsignal_svr3::V4Status::Missing) => {
Self::DataMissing
}
LogicError::Oprf(_)
| LogicError::Ppss(_, _)
| LogicError::BadData
LogicError::BadData
| LogicError::BadResponse
| LogicError::NumServers { .. }
| LogicError::NoUsableVersion

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
fn main() {
let protos = ["src/proto/svr3.proto", "src/proto/svr4.proto"];
let protos = ["src/proto/svr4.proto"];
prost_build::compile_protos(&protos, &["src"]).expect("Protobufs in src are valid");
for proto in &protos {
println!("cargo:rerun-if-changed={}", proto);

File diff suppressed because it is too large Load Diff

View File

@ -6,16 +6,10 @@ use std::fmt;
use prost::DecodeError;
pub use crate::oprf::errors::OPRFError;
pub use crate::ppss::PPSSError;
use crate::proto::svr4;
#[derive(Debug, displaydoc::Display, PartialEq)]
pub enum Error {
/// OPRF error: {0}
Oprf(OPRFError),
/// PPSS error: {0}, {1} tries remaining
Ppss(PPSSError, u32),
/// Invalid protobuf
BadData,
/// Unexpected or missing server response
@ -49,12 +43,6 @@ pub enum ErrorStatus {
impl std::error::Error for Error {}
impl From<OPRFError> for Error {
fn from(err: OPRFError) -> Self {
Self::Oprf(err)
}
}
impl From<DecodeError> for Error {
fn from(_err: DecodeError) -> Self {
Self::BadData

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
pub mod ciphersuite;
pub mod client;
pub mod errors;
mod util;

View File

@ -1,93 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek::ristretto::RistrettoPoint;
use super::util::expand_message_xmd_sha512;
const HASH_TO_GROUP_DST: &str = "HashToGroup-OPRFV1-\0-ristretto255-SHA512";
pub fn hash_to_group(data: &[u8]) -> RistrettoPoint {
let dst = HASH_TO_GROUP_DST.as_bytes();
let mut uniform_bytes = [0u8; 64];
expand_message_xmd_sha512(data, dst, 64u16, &mut uniform_bytes).unwrap();
RistrettoPoint::from_uniform_bytes(&uniform_bytes)
}
#[cfg(test)]
pub mod tests {
use curve25519_dalek::constants;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use hex_literal::hex;
use crate::oprf::errors::OPRFError;
use crate::oprf::util::{expand_message_xmd_sha512, i2osp_u16};
const DERIVE_KEYPAIR_DST: &str = "DeriveKeyPairOPRFV1-\0-ristretto255-SHA512";
fn is_zero(bytes: &[u8]) -> bool {
bytes.iter().all(|b| *b == 0)
}
pub fn derive_key_pair(
seed: &[u8],
info: &[u8],
) -> Result<(Scalar, RistrettoPoint), OPRFError> {
let mut derive_input = Vec::<u8>::with_capacity(seed.len() + info.len() + 3);
let info_len_u16 = match info.len().try_into() {
Ok(len) => len,
Err(_) => {
return Err(OPRFError::DeriveKeyPairError);
}
};
derive_input.extend_from_slice(seed);
derive_input.extend_from_slice(&i2osp_u16(info_len_u16));
derive_input.extend_from_slice(info);
derive_input.extend_from_slice(&[0u8]);
let len = derive_input.len();
let mut uniform_bytes = [0u8; 64];
for counter in 0..=255u8 {
derive_input[len - 1] = counter;
expand_message_xmd_sha512(
derive_input.as_slice(),
DERIVE_KEYPAIR_DST.as_bytes(),
64u16,
&mut uniform_bytes,
)
.unwrap();
if !is_zero(&uniform_bytes) {
let sk = Scalar::from_bytes_mod_order_wide(&uniform_bytes);
let pk = sk * constants::RISTRETTO_BASEPOINT_POINT;
return Ok((sk, pk));
}
}
Err(OPRFError::DeriveKeyPairError)
}
#[test]
fn test_ietf_a_1_1() {
let seed = hex!(
"
a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3
"
);
let key_info = hex!(
"
74657374206b6579
"
);
let sk_expected = hex!(
"
5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e
"
);
let (sk, _) = derive_key_pair(&seed, &key_info).unwrap();
assert_eq!(sk, Scalar::from_bytes_mod_order(sk_expected));
}
}

View File

@ -1,198 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use curve25519_dalek::traits::Identity;
use rand_core::CryptoRngCore;
use sha2::{Digest, Sha512};
use super::ciphersuite::hash_to_group;
use super::errors::OPRFError;
use super::util::i2osp_u16;
pub fn apply_blind(oprf_input: &[u8], blind: &Scalar) -> RistrettoPoint {
let input_element = hash_to_group(oprf_input);
blind * input_element
}
pub fn blind<R: CryptoRngCore>(
oprf_input: &[u8],
rng: &mut R,
) -> Result<(Scalar, RistrettoPoint), OPRFError> {
let blind = Scalar::random(rng);
let blinded_element = apply_blind(oprf_input, &blind);
if blinded_element == RistrettoPoint::identity() {
Err(OPRFError::BlindError)
} else {
Ok((blind, blinded_element))
}
}
pub fn finalize(oprf_input: &[u8], blind: &Scalar, evaluated_element: &RistrettoPoint) -> [u8; 64] {
let unblinded_element = blind.invert() * evaluated_element;
let compressed = unblinded_element.compress();
let unblinded_bytes = compressed.as_bytes();
let hasher = Sha512::new();
hasher
.chain_update(i2osp_u16(oprf_input.len().try_into().unwrap()))
.chain_update(oprf_input)
.chain_update(i2osp_u16(unblinded_bytes.len().try_into().unwrap()))
.chain_update(unblinded_bytes)
.chain_update("Finalize")
.finalize()
.as_slice()
.try_into()
.expect("Wrong length")
}
#[cfg(test)]
mod tests {
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use hex_literal::hex;
use crate::oprf::ciphersuite::tests::derive_key_pair;
use crate::oprf::client::{apply_blind, finalize};
fn blind_evaluate(sk: &Scalar, blinded_element: &RistrettoPoint) -> RistrettoPoint {
sk * blinded_element
}
fn ietf_test(
seed: &[u8],
key_info: &[u8],
sk_expected: [u8; 32],
input: &[u8],
blind_bytes: [u8; 32],
blinded_element_expected: &[u8],
evaluated_element_expected: &[u8],
output_expected: &[u8],
) {
let blind = Scalar::from_bytes_mod_order(blind_bytes);
let (sk, _) = derive_key_pair(seed, key_info).unwrap();
assert_eq!(sk, Scalar::from_bytes_mod_order(sk_expected));
let blinded_element = apply_blind(input, &blind);
let compressed_blinded = blinded_element.compress();
assert_eq!(compressed_blinded.as_bytes(), &blinded_element_expected);
let evaluated_element = blind_evaluate(&sk, &blinded_element);
let compressed_evaluated = evaluated_element.compress();
assert_eq!(compressed_evaluated.as_bytes(), &evaluated_element_expected);
let output = finalize(input, &blind, &evaluated_element);
assert_eq!(output, output_expected);
}
#[test]
fn ietf_a_1_1_1() {
let seed = hex!(
"
a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3
"
);
let key_info = hex!(
"
74657374206b6579
"
);
let sk_expected = hex!(
"
5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e
"
);
let input = hex!("00");
let blind_bytes = hex!(
"
64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706
"
);
let blinded_element_expected = hex!(
"
609a0ae68c15a3cf6903766461307e5c8bb2f95e7e6550e1ffa2dc99e412803c
"
);
let evaluated_element_expected = hex!(
"
7ec6578ae5120958eb2db1745758ff379e77cb64fe77b0b2d8cc917ea0869c7e
"
);
let output_expected = hex!("
527759c3d9366f277d8c6020418d96bb393ba2afb20ff90df23fb7708264e2f3ab9135e3bd69955851de4b1f9fe8a0973396719b7912ba9ee8aa7d0b5e24bcf6
");
ietf_test(
&seed,
&key_info,
sk_expected,
&input,
blind_bytes,
&blinded_element_expected,
&evaluated_element_expected,
&output_expected,
)
}
#[test]
fn ietf_a_1_1_2() {
let seed = hex!(
"
a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3
"
);
let key_info = hex!(
"
74657374206b6579
"
);
let sk_expected = hex!(
"
5ebcea5ee37023ccb9fc2d2019f9d7737be85591ae8652ffa9ef0f4d37063b0e
"
);
let input = hex!("5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a");
let blind_bytes = hex!(
"
64d37aed22a27f5191de1c1d69fadb899d8862b58eb4220029e036ec4c1f6706
"
);
let blinded_element_expected = hex!(
"
da27ef466870f5f15296299850aa088629945a17d1f5b7f5ff043f76b3c06418
"
);
let evaluated_element_expected = hex!(
"
b4cbf5a4f1eeda5a63ce7b77c7d23f461db3fcab0dd28e4e17cecb5c90d02c25
"
);
let output_expected = hex!("
f4a74c9c592497375e796aa837e907b1a045d34306a749db9f34221f7e750cb4f2a6413a6bf6fa5e19ba6348eb673934a722a7ede2e7621306d18951e7cf2c73
");
ietf_test(
&seed,
&key_info,
sk_expected,
&input,
blind_bytes,
&blinded_element_expected,
&evaluated_element_expected,
&output_expected,
)
}
}

View File

@ -1,24 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::fmt;
#[derive(Debug, PartialEq)]
pub enum OPRFError {
ExpandMessageError,
DeriveKeyPairError,
BlindError,
}
impl std::error::Error for OPRFError {}
impl fmt::Display for OPRFError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
OPRFError::ExpandMessageError => write!(f, "Expand Message Error"),
OPRFError::DeriveKeyPairError => write!(f, "Derive Key Pair Error"),
OPRFError::BlindError => write!(f, "Blinding Error"),
}
}
}

View File

@ -1,137 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::cmp;
use std::convert::{TryFrom, TryInto};
use sha2::{Digest, Sha512};
use super::errors::OPRFError;
const SHA512_BLOCK_BYTES: usize = 128usize;
const SHA512_OUTPUT_BYTES: usize = 64usize;
pub fn i2osp_u8(n: u8) -> [u8; 1] {
n.to_be_bytes()
}
pub fn i2osp_u16(n: u16) -> [u8; 2] {
n.to_be_bytes()
}
fn block_xor(
lhs: [u8; SHA512_OUTPUT_BYTES],
rhs: [u8; SHA512_OUTPUT_BYTES],
) -> [u8; SHA512_OUTPUT_BYTES] {
let mut result = [0u8; SHA512_OUTPUT_BYTES];
for i in 0..SHA512_OUTPUT_BYTES {
result[i] = lhs[i] ^ rhs[i];
}
result
}
pub fn expand_message_xmd_sha512(
msg: &[u8],
dst: &[u8],
len_in_bytes: u16,
result: &mut [u8],
) -> Result<(), OPRFError> {
if len_in_bytes == 0 || usize::from(len_in_bytes) != result.len() {
return Err(OPRFError::ExpandMessageError);
}
let b_in_bytes: u16 = SHA512_OUTPUT_BYTES.try_into().unwrap();
let ell = u8::try_from((len_in_bytes + b_in_bytes - 1) / b_in_bytes)
.map_err(|_| OPRFError::ExpandMessageError)?;
let l_i_b_arr = i2osp_u16(len_in_bytes);
let z_pad = [0u8; SHA512_BLOCK_BYTES];
let zero_byte = [0u8];
// msg_prime = z_pad + msg + l_i_b_str + I2OSP(0,1) + dst_prime
let b0_hasher = Sha512::new();
let b0: [u8; SHA512_OUTPUT_BYTES] = b0_hasher
.chain_update(z_pad)
.chain_update(msg)
.chain_update(l_i_b_arr)
.chain_update(zero_byte)
.chain_update(dst)
.chain_update(i2osp_u8(dst.len().try_into().unwrap()))
.finalize()
.into();
let b1_hasher = Sha512::new();
let b1: [u8; SHA512_OUTPUT_BYTES] = b1_hasher
.chain_update(b0)
.chain_update([1u8])
.chain_update(dst)
.chain_update(i2osp_u8(dst.len().try_into().unwrap()))
.finalize()
.into();
let bytes_to_copy = cmp::min(SHA512_OUTPUT_BYTES, usize::from(len_in_bytes));
result[0..bytes_to_copy].copy_from_slice(&b1[0..bytes_to_copy]);
let mut b_last = b1;
for i in 2..=ell {
let hasher = Sha512::new();
let b_next: [u8; SHA512_OUTPUT_BYTES] = hasher
.chain_update(block_xor(b0, b_last))
.chain_update([i])
.chain_update(dst)
.chain_update(i2osp_u8(dst.len().try_into().unwrap()))
.finalize()
.into();
let offset = usize::from(i - 1) * SHA512_OUTPUT_BYTES;
let bytes_to_copy = cmp::min(SHA512_OUTPUT_BYTES, usize::from(len_in_bytes) - offset);
result[offset..offset + bytes_to_copy].copy_from_slice(&b_next[0..bytes_to_copy]);
b_last.copy_from_slice(&b_next);
}
Ok(())
}
#[cfg(test)]
mod tests {
use hex_literal::hex;
#[test]
fn expand_message_xmd_1() {
let dst = "QUUX-V01-CS02-with-expander-SHA512-256";
let msg = "abc";
let len_in_bytes = 0x80u16;
let mut uniform_bytes = [0u8; 0x80];
super::expand_message_xmd_sha512(
msg.as_bytes(),
dst.as_bytes(),
len_in_bytes,
&mut uniform_bytes,
)
.expect("expand failed");
assert_eq!(uniform_bytes, hex!("
7f1dddd13c08b543f2e2037b14cefb255b44c83cc397c1786d975653e36a6b11bdd7732d8b38adb4a0edc26a0cef4bb45217135456e58fbca1703cd6032cb1347ee720b87972d63fbf232587043ed2901bce7f22610c0419751c065922b488431851041310ad659e4b23520e1772ab29dcdeb2002222a363f0c2b1c972b3efe1
"));
}
#[test]
fn expand_message_xmd_2() {
let dst = "QUUX-V01-CS02-with-expander-SHA512-256";
let msg = "abcdef0123456789";
let len_in_bytes = 0x20u16;
let mut uniform_bytes = [0u8; 0x20];
super::expand_message_xmd_sha512(
msg.as_bytes(),
dst.as_bytes(),
len_in_bytes,
&mut uniform_bytes,
)
.expect("expand failed");
assert_eq!(
uniform_bytes,
hex!(
"
087e45a86e2939ee8b91100af1583c4938e0f5fc6c9db4b107b83346bc967f58
"
)
);
}
}

View File

@ -1,417 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
//! Implements the Password Protected secret Sharing (PPSS) scheme of
//! [JKKX16](https://eprint.iacr.org/2016/144.pdf) using XOR-based secret sharing.
use std::convert::TryInto;
use curve25519_dalek::ristretto::CompressedRistretto;
use curve25519_dalek::Scalar;
use displaydoc::Display;
use hkdf::Hkdf;
use rand_core::CryptoRngCore;
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use crate::oprf;
use crate::oprf::errors::OPRFError;
#[derive(Display, Debug, PartialEq)]
pub enum PPSSError {
/// Invalid commitment, cannot reconstruct secret.
InvalidCommitment,
/// OPRF server output must encode canonical Ristretto points.
BadPointEncoding,
/// {0}
LengthMismatch(&'static str),
}
impl std::error::Error for PPSSError {}
type Key = [u8; 32];
type KeyShare = [u8; 32];
type Secret256 = [u8; 32];
fn arr_xor<const N: usize>(lhs: &[u8; N], rhs: &[u8], dest: &mut [u8; N]) {
for i in 0..N {
dest[i] = lhs[i] ^ rhs[i];
}
}
fn arr_xor_assign<const N: usize>(src: &[u8; N], acc: &mut [u8; N]) {
for i in 0..N {
acc[i] ^= src[i];
}
}
fn create_xor_keyshares<R: CryptoRngCore>(
secret: &Secret256,
n: usize,
rng: &mut R,
) -> Vec<KeyShare> {
let mut result = Vec::<KeyShare>::with_capacity(n);
// An accumulator keyshare
let mut acc = *secret;
for _ in 0..(n - 1) {
let mut data = [0u8; 32];
rng.fill_bytes(&mut data);
arr_xor_assign(&data, &mut acc);
result.push(data);
}
result.push(acc);
result
}
fn combine_xor_keyshares(keyshares: &[KeyShare]) -> Secret256 {
let mut secret = [0u8; 32];
for share in keyshares {
arr_xor_assign(share, &mut secret)
}
secret
}
// OPRF evaluation
/// An `OPRFSession` holds public information that a client needs to send a request
/// to the OPRF server as well as private information that will be needed to process
/// the server's response.
pub struct OPRFSession {
pub server_id: u64,
pub blinded_elt_bytes: [u8; 32],
blind: Scalar,
oprf_input: Vec<u8>,
}
fn prepare_oprf_input(context: &'static str, server_id: u64, input: &str) -> Vec<u8> {
let mut oprf_input_bytes = Vec::<u8>::new();
oprf_input_bytes.extend_from_slice(context.as_bytes());
oprf_input_bytes.extend_from_slice(&server_id.to_le_bytes());
oprf_input_bytes.extend_from_slice(input.as_bytes());
oprf_input_bytes
}
fn oprf_session_from_inputs<R: CryptoRngCore>(
context: &'static str,
server_id: u64,
input: &str,
rng: &mut R,
) -> Result<OPRFSession, OPRFError> {
let oprf_input = prepare_oprf_input(context, server_id, input);
let (blind, blinded_elt) = oprf::client::blind(&oprf_input, rng)?;
Ok(OPRFSession {
server_id,
blind,
blinded_elt_bytes: blinded_elt.compress().to_bytes(),
oprf_input,
})
}
/// Prepare OPRF requests for a given context and input to send to a list of servers.
///
/// # Errors
/// Returns `OPRFError::BlindError` if a computed blinded element turns out to be the identity.
/// This is would happen if the OPRF input were constructed so that it hashed to the identity.
pub fn begin_oprfs<R: CryptoRngCore>(
context: &'static str,
server_ids: &[u64],
input: &str,
rng: &mut R,
) -> Result<Vec<OPRFSession>, OPRFError> {
server_ids
.iter()
.map(|sid| oprf_session_from_inputs(context, *sid, input, rng))
.collect()
}
fn finalize_single_oprf(session: OPRFSession, bytes: &[u8; 32]) -> Result<[u8; 64], PPSSError> {
// deserialize the Ristretto point
let evaluated_elt = CompressedRistretto(*bytes)
.decompress()
.ok_or(PPSSError::BadPointEncoding)?;
Ok(oprf::client::finalize(
&session.oprf_input,
&session.blind,
&evaluated_elt,
))
}
/// Process OPRF server responses using the `OPRFSessions` created by `begin_oprfs`.
///
/// The order of `evaluated_elts` must correspond to the order of the
/// `OPRFSessions`s each session holds a public `server_id` and
/// `blinded_element` and the N-th element of `evaluated_elts` must be the
/// result of the server in the N-th session acting on the `blinded_elt_bytes`
/// of the N-th session.
///
/// # Errors
/// Returns `PPSSError::BadPointEncoding` if some member of `evaluated_elts`
/// is not a canonical encoding of Ristretto point.
pub fn finalize_oprfs(
sessions: Vec<OPRFSession>,
evaluated_elts: impl IntoIterator<Item = [u8; 32]>,
) -> Result<Vec<[u8; 64]>, PPSSError> {
std::iter::zip(sessions, evaluated_elts)
.map(|(session, bytes)| finalize_single_oprf(session, &bytes))
.collect()
}
// Password Protected Secret Sharing (PPSS) functions
/// A `MaskedShareSet` contains the information needed to restore a secret using a password.
#[derive(Clone, Debug)]
pub struct MaskedShareSet {
pub server_ids: Vec<u64>,
pub masked_shares: Vec<KeyShare>,
pub commitment: [u8; 32],
}
fn compute_commitment(
context: &'static str,
password: &[u8],
shares: Vec<KeyShare>,
masked_shares: &[KeyShare],
r: &[u8],
) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher = hasher
.chain_update(context.as_bytes())
.chain_update(b"commitment")
.chain_update(password);
//add the masked shares
for ms in masked_shares {
hasher.update(ms);
}
// add the secret shares
for s in shares {
hasher.update(s);
}
hasher.update(r);
hasher.finalize().into()
}
fn derive_key_and_bits_from_secret(secret: &Secret256, context: &'static str) -> [u8; 64] {
let hk = Hkdf::<Sha256>::new(None, secret);
let mut r_and_k = [0u8; 64];
hk.expand_multi_info(&[context.as_bytes(), b"keygen"], &mut r_and_k)
.expect("hkdf requested an invalid length.");
r_and_k
}
// Initialize a PPSS session
/// After evaluating OPRFs on a list of servers to get `oprf_outputs`, call `backup_secret` to create a
/// password-protected backup of the secret.
pub fn backup_secret<R: CryptoRngCore>(
context: &'static str,
password: &[u8],
server_ids: Vec<u64>,
oprf_outputs: Vec<[u8; 64]>,
secret: &Secret256,
rng: &mut R,
) -> Result<MaskedShareSet, PPSSError> {
if server_ids.len() != oprf_outputs.len() {
return Err(PPSSError::LengthMismatch(
"Number of OPRF outputs does not match that of server ids",
));
}
let shares = create_xor_keyshares(secret, oprf_outputs.len(), rng);
let masked_shares: Vec<[u8; 32]> = shares
.iter()
.zip(oprf_outputs.iter())
.map(|(share, mask)| {
let mut masked = [0u8; 32];
arr_xor(share, &mask[..32], &mut masked);
masked
})
.collect();
let r_and_k = derive_key_and_bits_from_secret(secret, context);
let r = &r_and_k[..32];
let commitment = compute_commitment(context, password, shares, &masked_shares, r);
Ok(MaskedShareSet {
server_ids,
masked_shares,
commitment,
})
}
/// Recover a secret with a PPSS share set. The `oprf_outputs` should be the result
/// of a call to `finalize_oprfs` and the order of the `server_ids` used in the call to
/// `finalize_oprfs` should match the order of the `server_ids` in `masked_shareset`.
///
/// # Errors
/// Returns `PPSSError::InvalidCommitment` when the reconstructed secret does not pass
/// integrity validation.
///
pub fn restore_secret(
context: &'static str,
password: &[u8],
oprf_outputs: Vec<[u8; 64]>,
masked_shareset: MaskedShareSet,
) -> Result<(Secret256, Key), PPSSError> {
if oprf_outputs.len() != masked_shareset.masked_shares.len() {
return Err(PPSSError::LengthMismatch(
"Number of OPRF outputs does not match that of masked shares",
));
}
let keyshares: Vec<[u8; 32]> = masked_shareset
.masked_shares
.iter()
.zip(oprf_outputs.iter())
.map(|(masked_share, mask)| {
let mut share = [0u8; 32];
arr_xor(masked_share, &mask[..32], &mut share);
share
})
.collect();
let secret = combine_xor_keyshares(keyshares.as_slice());
let r_and_k = derive_key_and_bits_from_secret(&secret, context);
let (r, k) = r_and_k.split_at(32);
let commitment = compute_commitment(
context,
password,
keyshares,
&masked_shareset.masked_shares,
r,
);
if commitment.ct_eq(&masked_shareset.commitment).into() {
Ok((secret, k.try_into().unwrap()))
} else {
Err(PPSSError::InvalidCommitment)
}
}
#[cfg(test)]
pub mod testutils {
use std::collections::HashMap;
use curve25519_dalek::scalar::Scalar;
use curve25519_dalek::RistrettoPoint;
use super::*;
pub struct OPRFServerSet {
server_secrets: HashMap<u64, [u8; 32]>,
}
impl OPRFServerSet {
pub fn new(server_ids: &[u64]) -> Self {
let server_secrets: HashMap<u64, [u8; 32]> = server_ids
.iter()
.cloned()
.map(|sid| (sid, zerocopy::transmute!([sid.to_le_bytes(); 4])))
.collect();
Self { server_secrets }
}
pub fn eval(&self, server_id: &u64, blinded_elt_bytes: &[u8; 32]) -> [u8; 32] {
let secret = Scalar::from_bytes_mod_order(*self.server_secrets.get(server_id).unwrap());
oprf_eval_bytes(&secret, blinded_elt_bytes)
}
}
pub const CONTEXT: &str = "signal-svr3-ppss-test";
fn oprf_eval(secret: &Scalar, blinded_elt: &RistrettoPoint) -> RistrettoPoint {
secret * blinded_elt
}
fn oprf_eval_bytes(secret: &Scalar, blinded_elt_bytes: &[u8; 32]) -> [u8; 32] {
let blinded_elt = CompressedRistretto::from_slice(blinded_elt_bytes)
.expect("can create compressed ristretto")
.decompress()
.expect("can decompress");
let eval_elt = oprf_eval(secret, &blinded_elt);
eval_elt.compress().to_bytes()
}
}
#[cfg(test)]
mod tests {
use super::testutils::*;
use super::*;
#[test]
fn store_reconstruct_xor_shares() {
let mut rng = rand_core::OsRng;
// set up constants - secret, oprf secrets
let secret = [42u8; 32];
let password = "supahsecretpassword";
let server_ids = vec![4u64, 1, 6];
let masked_shareset = {
let oprf_servers = OPRFServerSet::new(&server_ids);
// get the blinds - they are in order of server_id
let oprfs = begin_oprfs(CONTEXT, &server_ids, password, &mut rng).unwrap();
// eval the oprfs
let eval_elt_bytes: Vec<[u8; 32]> = oprfs
.iter()
.map(|session| oprf_servers.eval(&session.server_id, &session.blinded_elt_bytes))
.collect();
let outputs = finalize_oprfs(oprfs, eval_elt_bytes)
.expect("oprf evaluated element encodings must be canonical");
backup_secret(
CONTEXT,
password.as_bytes(),
server_ids.clone(),
outputs,
&secret,
&mut rng,
)
.unwrap()
};
let (restored_secret, restored_key) = {
let oprf_servers = OPRFServerSet::new(&server_ids);
// Now reconstruct
let oprfs =
begin_oprfs(CONTEXT, &masked_shareset.server_ids, password, &mut rng).unwrap();
// eval the oprfs
let eval_elt_bytes: Vec<[u8; 32]> = oprfs
.iter()
.map(|session| oprf_servers.eval(&session.server_id, &session.blinded_elt_bytes))
.collect();
let outputs = finalize_oprfs(oprfs, eval_elt_bytes)
.expect("oprf evaluated element encodings must be canonical");
restore_secret(CONTEXT, password.as_bytes(), outputs, masked_shareset)
.expect("valid commitment")
};
assert_eq!(secret, restored_secret);
let r_and_k = derive_key_and_bits_from_secret(&secret, CONTEXT);
assert_eq!(&r_and_k[32..64], &restored_key);
}
#[test]
fn backup_length_mismatch() {
let mut rng = rand_core::OsRng;
let secret = [0; 32];
assert!(matches!(
backup_secret(CONTEXT, b"password", vec![42], vec![], &secret, &mut rng),
Err(PPSSError::LengthMismatch(_))
));
}
#[test]
fn restore_length_mismatch() {
let share_set = MaskedShareSet {
server_ids: vec![42],
masked_shares: vec![[0u8; 32]],
commitment: [1u8; 32],
};
assert!(matches!(
restore_secret(CONTEXT, b"password", vec![], share_set),
Err(PPSSError::LengthMismatch(_))
));
}
}

View File

@ -2,5 +2,4 @@
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
pub mod svr3;
pub mod svr4;

View File

@ -1,93 +0,0 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
syntax = "proto3";
package svr3.client;
option optimize_for = LITE_RUNTIME;
// Client protocol for SVR3.
message Request {
oneof inner {
CreateRequest create = 1;
EvaluateRequest evaluate = 2;
RemoveRequest remove = 3;
QueryRequest query = 4;
}
}
message Response {
oneof inner {
CreateResponse create = 1;
EvaluateResponse evaluate = 2;
RemoveResponse remove = 3;
QueryResponse query = 4;
}
}
//
// create
//
message CreateRequest {
uint32 max_tries = 1;
bytes blinded_element = 2; // ristretto255 element, 32 bytes
}
message CreateResponse {
enum Status {
UNSET = 0;
OK = 1;
INVALID_REQUEST = 2;
ERROR = 3;
}
Status status = 1;
bytes evaluated_element = 2; // ristretto255 element, 32 bytes
}
//
// evaluate
//
message EvaluateRequest {
bytes blinded_element = 1; // ristretto255 element, 32 bytes
}
message EvaluateResponse {
enum Status {
UNSET = 0;
OK = 1;
MISSING = 2;
INVALID_REQUEST = 3;
ERROR = 4;
}
Status status = 1;
bytes evaluated_element = 2; // ristretto255 element, 32 bytes
uint32 tries_remaining = 3;
}
//
// remove
//
message RemoveRequest {
}
message RemoveResponse {
}
//
// query
//
message QueryRequest {
}
message QueryResponse {
enum Status {
UNSET = 0;
OK = 1;
MISSING = 2;
}
Status status = 1;
uint32 tries_remaining = 2;
}

View File

@ -1,8 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
#![allow(clippy::derive_partial_eq_without_eq)]
include!(concat!(env!("OUT_DIR"), "/svr3.client.rs"));