-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Constant-time square root and division #277
Conversation
The initial guess is 2^(ceil(bits/2)), which is always greater than sqrt(self), so initially `xn < guess`, which means the removed while loop never runs.
This un-deprecates sqrt, makes it non-const (to be able to use checked_div), and provides a constant-time implementation using essentially the same algorithm as sqrt_vartime, but run for a fixed number of iterations.
arXiv perhaps? |
Sure, I can upload it to arXiv (cs.DS probably?). I'd like to wait until at least one other person has looked it over it reduce the chance of errors slipping through, but after that I'll go ahead. |
cc @fjarri |
let cap = Self::ONE.shl(max_bits); | ||
let mut guess = cap; // ≥ √(`self`) | ||
let mut xn = { | ||
let q = self.wrapping_div(&guess); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think wrapping_div()
is currently constant-time (although it could be made so).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mistake, I didn't see that wrapping_div
is only constant-time for a fixed rhs
. Actually, I think there might be a documentation issue here—I can't find anywhere in the public documentation that says this, only the documentation for ct_div_rem
(which doesn't appear in the public docs since it's pub(crate)
).
What do you think would be the better approach here: making a new function that's like ct_div_rem
but constant-time with respect to both inputs, or modifying ct_div_rem
to have this stronger constant-time guarantee and moving the "constant-time only for fixed rhs" behavior to a new function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that the name is misleading - it should have had a vartime
suffix (and the whole div.rs
is kind of a mess in terms of naming - see #268). So the proper way to proceed I think would be to rename the current one to _vartime
, and implement a constant-time one in its place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears this is a blocker on merging this PR. I guess we can go ahead and flip over to the v0.6 series per #268 and try to land this PR afterward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking on this: wrapping_div
calls const_div_rem
, which claims:
/// This function is constant-time with respect to both `self` and `rhs`.
pub(crate) const fn const_div_rem(&self, rhs: &Self) -> (Self, Self, CtChoice) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which commit are you looking at? The current master
has no const_div_rem()
, and Uint::wrapping_div()
uses ct_div_rem()
, which is not constant-time in rhs
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR changes wrapping_div
to call const_div_rem
, which is also added by this PR
I am not sure I see the problem here, you can always use the underlying |
I just overlooked that, so there's no obstruction to keeping it pub(crate) const fn ct_div_rem(&self, rhs: &Self) -> (Self, Self, CtChoice) {
let mb = rhs.bits_vartime();
// [...]
let is_some = Limb(mb as Word).ct_is_nonzero();
quo = Self::ct_select(&Self::ZERO, &quo, is_some);
(quo, rem, is_some)
} |
I think it should be treated as an implementation detail. |
Also:
|
Okay, I just made the necessary changes so |
This makes `Uint::sqrt` constant-time as well.
Okay, I added an implementation of division that's constant-time in both arguments, and moved the old division implementation (that's constant-time only for fixed divisor) to My constant-time division implementation is a straightforward modification of the vartime algorithm, just replacing vartime methods with their constant-time variants, and running for the maximum number of iterations with a |
// Repeat enough times to guarantee result has stabilized. | ||
// See Hast, "Note on computation of integer square roots" for a proof of this bound. | ||
let mut i = 0; | ||
while i < usize::BITS - Self::BITS.leading_zeros() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self::LOG2_BITS could be used here
}; | ||
|
||
// Repeat enough times to guarantee result has stabilized. | ||
// See Hast, "Note on computation of integer square roots" for a proof of this bound. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When your paper has a more permanent link (e.g. on arxiv), please make a PR referencing it here
guess = xn; | ||
xn = { | ||
let (q, _, is_some) = self.const_div_rem(&guess); | ||
let q = Self::ct_select(&Self::ZERO, &q, is_some); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this needed specifically to handle the case of self == 0
?
i += 1; | ||
} | ||
|
||
// at least one of `guess` and `xn` is now equal to √(`self`), so return the minimum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So at this point guess == x_n
, and xn == x_{n+1}
, where n = floor(log2(Self::BITS))
. But in the paper it says that it should be n = floor(log2(Self::BITS)) + 1
- am I missing something?
t.shr_vartime(1) | ||
}; | ||
} | ||
// Note, xn <= guess at this point. | ||
|
||
// Repeat while guess decreases. | ||
while Uint::ct_gt(&guess, &xn).is_true_vartime() && xn.ct_is_nonzero().is_true_vartime() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to use ct_gt()
and ct_is_nonzero()
here, those are constant-time
@@ -56,29 +64,26 @@ impl<const LIMBS: usize> Uint<LIMBS> { | |||
Self::ct_select(&Self::ZERO, &guess, self.ct_is_nonzero()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly here, we don't need constant-timeness.
Sorry for the delay, I was kind of swamped at work. I've read the paper, and it looks sound to me (although a bit too terse :). Have you submitted it anywhere yet? Would be nice to have a reference in the comments to the code. I've also got some relatively minor comments to the code. |
I think with a bit more polish on it you could throw it on the IACR ePrint service. |
for i in 0..half.limbs.len() / 2 { | ||
half.limbs[i] = Limb::MAX; | ||
} | ||
assert_eq!(U256::MAX.sqrt(), half); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An idea for the edge case test: a number that actually needs the maximum amount of iterations to converge. According to my tests, the ones before 10000 are 80, 99, 4224, 4355, 4488, 4623, 4760, 4899; but please check independently. (Also an interesting mathematical question - is there some rule for their distribution)
Based on PR #277. The constant-time square root algorithm is described here: https://github.com/RustCrypto/crypto-bigint/files/12600669/ct_sqrt.pdf Co-authored-by: Daniel Hast <32797673+HastD@users.noreply.github.com>
Merged in #376 |
Adapts the implementation originally from #277 to `BoxedUint`, adding the following methods: - `BoxedUint::div_rem` - `BoxedUint::rem` Additionally, `wrapping_div` and `checked_div` have been changed to use the constant-time versions, rather than `*_vartime`.
Adapts the implementation originally from #277 to `BoxedUint`, adding the following methods: - `BoxedUint::div_rem` - `BoxedUint::rem` Additionally, `wrapping_div` and `checked_div` have been changed to use the constant-time versions, rather than `*_vartime`.
Adapts the implementation originally from #277 to `BoxedUint`, adding the following methods: - `BoxedUint::div_rem` - `BoxedUint::rem` Additionally, `wrapping_div` and `checked_div` have been changed to use the constant-time versions, rather than `*_vartime`.
This un-deprecates
Uint::sqrt
and provides a constant-time implementation.(Unfortunately this makes it non-[Edit: this isn't an issue after all, see below discussion.] The algorithm is just a slight modification of Algorithm 1.13 of Brent-Zimmermann as used inconst
, so it's technically a breaking change; I don't see a way to handle zero without usingchecked_div
, which is why this can't beconst
. Forconst
contexts,Uint::sqrt_vartime
can still be used.)sqrt_vartime
: instead of iterating untilxn
increases, we just iterate a fixed number of times (logarithmic in the number of bits) and then take the minimum ofguess
andxn
at the end.The reason this works is because of two properties of this algorithm that I prove here: ct_sqrt.pdf. (We can upload this somewhere more permanent—not sure where would be best. Also, it'd be great if someone could look this over to make sure I didn't make any mistakes in the proof.) First, we have an upper bound the number of iterations required. Second, once the algorithm reaches the desired value
sqrt(self)
, it will either repeatsqrt(self)
or it will alternate betweensqrt(self)
andsqrt(self) + 1
; thus, we can return the minimum ofguess
andxn
.I also removed some dead code in
Uint::sqrt_vartime
: unless I'm really missing something here, the firstwhile
loop can never run because the initial value ofguess
is defined such that it's never an underestimate.Edit: Just noticed I forgot to un-deprecate
checked_sqrt
andwrapping_sqrt
and make those point tosqrt
rather thansqrt_vartime
, but obviously that goes along with this and I've added those changes too.