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:
parent
1c2113617f
commit
c65a5763df
@ -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
|
||||
|
@ -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
@ -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
|
||||
|
2155
rust/svr3/src/lib.rs
2155
rust/svr3/src/lib.rs
File diff suppressed because it is too large
Load Diff
@ -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;
|
@ -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));
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -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(_))
|
||||
));
|
||||
}
|
||||
}
|
@ -2,5 +2,4 @@
|
||||
// Copyright 2023 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
pub mod svr3;
|
||||
pub mod svr4;
|
||||
|
@ -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;
|
||||
}
|
@ -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"));
|
Loading…
Reference in New Issue
Block a user