Utilize zeroize (#76)

* Apply Zeroize to nonces used in Bulletproofs

Also makes bit decomposition constant time for a given amount of 
outputs.

* Fix nonce reuse for single-signer CLSAG

* Attach Zeroize to most structures in Monero, and ZOnDrop to anything with private data

* Zeroize private keys and nonces

* Merge prepare_outputs and prepare_transactions

* Ensure CLSAG is constant time

* Pass by borrow where needed, bug fixes

The past few commitments have been one in-progress chunk which I've 
broken up as best read.

* Add Zeroize to FROST structs

Still needs to zeroize internally, yet next step. Not quite as 
aggressive as Monero, partially due to the limitations of HashMaps, 
partially due to less concern about metadata, yet does still delete a 
few smaller items of metadata (group key, context string...).

* Remove Zeroize from most Monero multisig structs

These structs largely didn't have private data, just fields with private 
data, yet those fields implemented ZeroizeOnDrop making them already 
covered. While there is still traces of the transaction left in RAM, 
fully purging that was never the intent.

* Use Zeroize within dleq

bitvec doesn't offer Zeroize, so a manual zeroing has been implemented.

* Use Zeroize for random_nonce

It isn't perfect, due to the inability to zeroize the digest, and due to 
kp256 requiring a few transformations. It does the best it can though.

Does move the per-curve random_nonce to a provided one, which is allowed 
as of https://github.com/cfrg/draft-irtf-cfrg-frost/pull/231.

* Use Zeroize on FROST keygen/signing

* Zeroize constant time multiexp.

* Correct when FROST keygen zeroizes

* Move the FROST keys Arc into FrostKeys

Reduces amount of instances in memory.

* Manually implement Debug for FrostCore to not leak the secret share

* Misc bug fixes

* clippy + multiexp test bug fixes

* Correct FROST key gen share summation

It leaked our own share for ourself.

* Fix cross-group DLEq tests
This commit is contained in:
Luke Parker
2022-08-03 03:25:18 -05:00
committed by GitHub
parent a30568ff57
commit 797be71eb3
56 changed files with 698 additions and 425 deletions

View File

@@ -10,6 +10,8 @@ edition = "2021"
thiserror = "1"
rand_core = "0.6"
zeroize = "1.3"
digest = "0.10"
transcript = { package = "flexible-transcript", path = "../transcript", version = "0.1" }

View File

@@ -1,5 +1,7 @@
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use transcript::Transcript;
use group::{
@@ -46,15 +48,16 @@ impl<G0: PrimeGroup, G1: PrimeGroup> Re<G0, G1> {
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Aos<G0: PrimeGroup, G1: PrimeGroup, const RING_LEN: usize> {
pub(crate) struct Aos<G0: PrimeGroup + Zeroize, G1: PrimeGroup + Zeroize, const RING_LEN: usize> {
Re_0: Re<G0, G1>,
s: [(G0::Scalar, G1::Scalar); RING_LEN],
}
impl<G0: PrimeGroup, G1: PrimeGroup, const RING_LEN: usize> Aos<G0, G1, RING_LEN>
impl<G0: PrimeGroup + Zeroize, G1: PrimeGroup + Zeroize, const RING_LEN: usize>
Aos<G0, G1, RING_LEN>
where
G0::Scalar: PrimeFieldBits,
G1::Scalar: PrimeFieldBits,
G0::Scalar: PrimeFieldBits + Zeroize,
G1::Scalar: PrimeFieldBits + Zeroize,
{
#[allow(non_snake_case)]
fn nonces<T: Transcript>(mut transcript: T, nonces: (G0, G1)) -> (G0::Scalar, G1::Scalar) {
@@ -102,8 +105,8 @@ where
transcript: T,
generators: (Generators<G0>, Generators<G1>),
ring: &[(G0, G1)],
actual: usize,
blinding_key: (G0::Scalar, G1::Scalar),
mut actual: usize,
blinding_key: &mut (G0::Scalar, G1::Scalar),
mut Re_0: Re<G0, G1>,
) -> Self {
// While it is possible to use larger values, it's not efficient to do so
@@ -113,7 +116,7 @@ where
let mut s = [(G0::Scalar::zero(), G1::Scalar::zero()); RING_LEN];
let r = (G0::Scalar::random(&mut *rng), G1::Scalar::random(&mut *rng));
let mut r = (G0::Scalar::random(&mut *rng), G1::Scalar::random(&mut *rng));
#[allow(non_snake_case)]
let original_R = (generators.0.alt * r.0, generators.1.alt * r.1);
#[allow(non_snake_case)]
@@ -135,6 +138,11 @@ where
if i == actual {
s[i] = (r.0 + (e.0 * blinding_key.0), r.1 + (e.1 * blinding_key.1));
debug_assert_eq!(Self::R(generators, s[i], ring[actual], e), original_R);
actual.zeroize();
blinding_key.0.zeroize();
blinding_key.1.zeroize();
r.0.zeroize();
r.1.zeroize();
break;
// Generate a decoy response
} else {

View File

@@ -1,5 +1,7 @@
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use transcript::Transcript;
use group::{ff::PrimeFieldBits, prime::PrimeGroup};
@@ -67,16 +69,25 @@ impl BitSignature {
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct Bits<G0: PrimeGroup, G1: PrimeGroup, const SIGNATURE: u8, const RING_LEN: usize> {
pub(crate) struct Bits<
G0: PrimeGroup + Zeroize,
G1: PrimeGroup + Zeroize,
const SIGNATURE: u8,
const RING_LEN: usize,
> {
pub(crate) commitments: (G0, G1),
signature: Aos<G0, G1, RING_LEN>,
}
impl<G0: PrimeGroup, G1: PrimeGroup, const SIGNATURE: u8, const RING_LEN: usize>
Bits<G0, G1, SIGNATURE, RING_LEN>
impl<
G0: PrimeGroup + Zeroize,
G1: PrimeGroup + Zeroize,
const SIGNATURE: u8,
const RING_LEN: usize,
> Bits<G0, G1, SIGNATURE, RING_LEN>
where
G0::Scalar: PrimeFieldBits,
G1::Scalar: PrimeFieldBits,
G0::Scalar: PrimeFieldBits + Zeroize,
G1::Scalar: PrimeFieldBits + Zeroize,
{
fn transcript<T: Transcript>(transcript: &mut T, i: usize, commitments: (G0, G1)) {
transcript.domain_separate(b"bits");
@@ -106,8 +117,8 @@ where
generators: (Generators<G0>, Generators<G1>),
i: usize,
pow_2: &mut (G0, G1),
bits: u8,
blinding_key: (G0::Scalar, G1::Scalar),
mut bits: u8,
blinding_key: &mut (G0::Scalar, G1::Scalar),
) -> Self {
let mut commitments =
((generators.0.alt * blinding_key.0), (generators.1.alt * blinding_key.1));
@@ -125,6 +136,7 @@ where
blinding_key,
BitSignature::from(SIGNATURE).aos_form(),
);
bits.zeroize();
Self::shift(pow_2);
Bits { commitments, signature }

View File

@@ -1,6 +1,9 @@
use thiserror::Error;
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use digest::Digest;
use transcript::Transcript;
@@ -73,8 +76,8 @@ pub enum DLEqError {
// anyone who wants it
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct __DLEqProof<
G0: PrimeGroup,
G1: PrimeGroup,
G0: PrimeGroup + Zeroize,
G1: PrimeGroup + Zeroize,
const SIGNATURE: u8,
const RING_LEN: usize,
const REMAINDER_RING_LEN: usize,
@@ -131,15 +134,15 @@ dleq!(EfficientLinearDLEq, BitSignature::EfficientLinear, false);
dleq!(CompromiseLinearDLEq, BitSignature::CompromiseLinear, true);
impl<
G0: PrimeGroup,
G1: PrimeGroup,
G0: PrimeGroup + Zeroize,
G1: PrimeGroup + Zeroize,
const SIGNATURE: u8,
const RING_LEN: usize,
const REMAINDER_RING_LEN: usize,
> __DLEqProof<G0, G1, SIGNATURE, RING_LEN, REMAINDER_RING_LEN>
where
G0::Scalar: PrimeFieldBits,
G1::Scalar: PrimeFieldBits,
G0::Scalar: PrimeFieldBits + Zeroize,
G1::Scalar: PrimeFieldBits + Zeroize,
{
pub(crate) fn transcript<T: Transcript>(
transcript: &mut T,
@@ -213,22 +216,27 @@ where
let mut pow_2 = (generators.0.primary, generators.1.primary);
let raw_bits = f.0.to_le_bits();
let mut raw_bits = f.0.to_le_bits();
let mut bits = Vec::with_capacity(capacity);
let mut these_bits: u8 = 0;
for (i, bit) in raw_bits.iter().enumerate() {
// Needed to zero out the bits
#[allow(unused_assignments)]
for (i, mut raw_bit) in raw_bits.iter_mut().enumerate() {
if i == capacity {
break;
}
let bit = *bit as u8;
let mut bit = *raw_bit as u8;
debug_assert_eq!(bit | 1, 1);
*raw_bit = false;
// Accumulate this bit
these_bits |= bit << (i % bits_per_group);
bit = 0;
if (i % bits_per_group) == (bits_per_group - 1) {
let last = i == (capacity - 1);
let blinding_key = blinding_key(&mut *rng, last);
let mut blinding_key = blinding_key(&mut *rng, last);
bits.push(Bits::prove(
&mut *rng,
transcript,
@@ -236,7 +244,7 @@ where
i / bits_per_group,
&mut pow_2,
these_bits,
blinding_key,
&mut blinding_key,
));
these_bits = 0;
}
@@ -245,7 +253,7 @@ where
let mut remainder = None;
if capacity != ((capacity / bits_per_group) * bits_per_group) {
let blinding_key = blinding_key(&mut *rng, true);
let mut blinding_key = blinding_key(&mut *rng, true);
remainder = Some(Bits::prove(
&mut *rng,
transcript,
@@ -253,10 +261,12 @@ where
capacity / bits_per_group,
&mut pow_2,
these_bits,
blinding_key,
&mut blinding_key,
));
}
these_bits.zeroize();
let proof = __DLEqProof { bits, remainder, poks };
debug_assert_eq!(
proof.reconstruct_keys(),

View File

@@ -1,7 +1,11 @@
use ff::PrimeFieldBits;
use zeroize::Zeroize;
/// Convert a uniform scalar into one usable on both fields, clearing the top bits as needed
pub fn scalar_normalize<F0: PrimeFieldBits, F1: PrimeFieldBits>(scalar: F0) -> (F0, F1) {
pub fn scalar_normalize<F0: PrimeFieldBits + Zeroize, F1: PrimeFieldBits>(
mut scalar: F0,
) -> (F0, F1) {
let mutual_capacity = F0::CAPACITY.min(F1::CAPACITY);
// The security of a mutual key is the security of the lower field. Accordingly, this bans a
@@ -13,30 +17,50 @@ pub fn scalar_normalize<F0: PrimeFieldBits, F1: PrimeFieldBits>(scalar: F0) -> (
let mut res2 = F1::zero();
// Uses the bit view API to ensure a consistent endianess
let mut bits = scalar.to_le_bits();
scalar.zeroize();
// Convert it to big endian
bits.reverse();
for bit in bits.iter().skip(bits.len() - usize::try_from(mutual_capacity).unwrap()) {
let mut skip = bits.len() - usize::try_from(mutual_capacity).unwrap();
// Needed to zero out the bits
#[allow(unused_assignments)]
for mut raw_bit in bits.iter_mut() {
if skip > 0 {
*raw_bit = false;
skip -= 1;
continue;
}
res1 = res1.double();
res2 = res2.double();
let bit = *bit as u8;
let mut bit = *raw_bit as u8;
debug_assert_eq!(bit | 1, 1);
*raw_bit = false;
res1 += F0::from(bit.into());
res2 += F1::from(bit.into());
bit = 0;
}
(res1, res2)
}
/// Helper to convert a scalar between fields. Returns None if the scalar isn't mutually valid
pub fn scalar_convert<F0: PrimeFieldBits, F1: PrimeFieldBits>(scalar: F0) -> Option<F1> {
let (valid, converted) = scalar_normalize(scalar);
Some(converted).filter(|_| scalar == valid)
pub fn scalar_convert<F0: PrimeFieldBits + Zeroize, F1: PrimeFieldBits>(
mut scalar: F0,
) -> Option<F1> {
let (mut valid, converted) = scalar_normalize(scalar);
let res = Some(converted).filter(|_| scalar == valid);
scalar.zeroize();
valid.zeroize();
res
}
/// Create a mutually valid scalar from bytes via bit truncation to not introduce bias
pub fn mutual_scalar_from_bytes<F0: PrimeFieldBits, F1: PrimeFieldBits>(bytes: &[u8]) -> (F0, F1) {
pub fn mutual_scalar_from_bytes<F0: PrimeFieldBits + Zeroize, F1: PrimeFieldBits>(
bytes: &[u8],
) -> (F0, F1) {
let capacity = usize::try_from(F0::CAPACITY.min(F1::CAPACITY)).unwrap();
debug_assert!((bytes.len() * 8) >= capacity);

View File

@@ -1,5 +1,7 @@
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use transcript::Transcript;
use group::{
@@ -19,14 +21,14 @@ use crate::{read_scalar, cross_group::read_point};
#[allow(non_snake_case)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub(crate) struct SchnorrPoK<G: PrimeGroup> {
pub(crate) struct SchnorrPoK<G: PrimeGroup + Zeroize> {
R: G,
s: G::Scalar,
}
impl<G: PrimeGroup> SchnorrPoK<G>
impl<G: PrimeGroup + Zeroize> SchnorrPoK<G>
where
G::Scalar: PrimeFieldBits,
G::Scalar: PrimeFieldBits + Zeroize,
{
// Not hram due to the lack of m
#[allow(non_snake_case)]
@@ -42,15 +44,18 @@ where
rng: &mut R,
transcript: &mut T,
generator: G,
private_key: G::Scalar,
mut private_key: G::Scalar,
) -> SchnorrPoK<G> {
let nonce = G::Scalar::random(rng);
let mut nonce = G::Scalar::random(rng);
#[allow(non_snake_case)]
let R = generator * nonce;
SchnorrPoK {
let res = SchnorrPoK {
R,
s: nonce + (private_key * SchnorrPoK::hra(transcript, generator, R, generator * private_key)),
}
};
private_key.zeroize();
nonce.zeroize();
res
}
pub(crate) fn verify<R: RngCore + CryptoRng, T: Transcript>(

View File

@@ -2,6 +2,8 @@
use rand_core::{RngCore, CryptoRng};
use zeroize::Zeroize;
use transcript::Transcript;
use ff::{Field, PrimeField};
@@ -76,9 +78,12 @@ impl<G: PrimeGroup> DLEqProof<G> {
rng: &mut R,
transcript: &mut T,
generators: &[G],
scalar: G::Scalar,
) -> DLEqProof<G> {
let r = G::Scalar::random(rng);
mut scalar: G::Scalar,
) -> DLEqProof<G>
where
G::Scalar: Zeroize,
{
let mut r = G::Scalar::random(rng);
transcript.domain_separate(b"dleq");
for generator in generators {
@@ -88,6 +93,9 @@ impl<G: PrimeGroup> DLEqProof<G> {
let c = challenge(transcript);
let s = r + (c * scalar);
scalar.zeroize();
r.zeroize();
DLEqProof { c, s }
}

View File

@@ -38,7 +38,7 @@ fn test_aos<const RING_LEN: usize>(default: Re<G0, G1>) {
generators,
&ring,
actual,
ring_keys[actual],
&mut ring_keys[actual],
default.clone(),
);

View File

@@ -1,5 +1,7 @@
use rand_core::OsRng;
use zeroize::Zeroize;
use group::{
ff::{Field, PrimeFieldBits},
prime::PrimeGroup,
@@ -10,9 +12,9 @@ use transcript::{Transcript, RecommendedTranscript};
use crate::cross_group::schnorr::SchnorrPoK;
fn test_schnorr<G: PrimeGroup>()
fn test_schnorr<G: PrimeGroup + Zeroize>()
where
G::Scalar: PrimeFieldBits,
G::Scalar: PrimeFieldBits + Zeroize,
{
let private = G::Scalar::random(&mut OsRng);