-
Notifications
You must be signed in to change notification settings - Fork 572
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
PQC: FrodoKEM #3679
PQC: FrodoKEM #3679
Conversation
a49f970
to
fe72599
Compare
fa74b71
to
1ae5b96
Compare
322b34e
to
e0c7fd8
Compare
ea6d352
to
3a043ff
Compare
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 did just a very quick look at the code, no detailed code review or trying to follow the Frodo algorithm.
// Multiply by s on the left | ||
// Inputs: b (N x N_BAR), s (N_BAR x N), e (N_BAR x N_BAR) | ||
// Output: out = s*b + e (N_BAR x N_BAR). The existing elements are overwritten and a self reference is returned. |
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.
Do you think this descriptions are more important for understanding the implementation than for a there the function is used, or why are they here?
Not completely against keeping this style here, but to me looks a lot like a description that belongs in a docstring.
Same for all the other functions in this file.
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.
Tbh we relied on it for the implementation of the function itself but it would also be useful when calling the function (since the necessary dimensions are shown). I put them into docstrings accordingly.
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.
You just moved them in the header, but they are not yet proper docstrings.
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 didn't compare the code against the specification. I only left some remarks. All in all, LGTM!
if args is None: | ||
args = sys.argv | ||
|
||
output = open('src/tests/data/pubkey/frodokem_kat.vec', 'w') |
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.
More modern Python would be: with open(...) as output:
.
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.
Super modern python would also be to add encoding='utf8'
. 😨 This script is more documentation than actual functionality, though. So not super important IMO.
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 is a first pass review of everything except the frodo_matrix.cpp
file
FrodoKEMConstants::FrodoKEMConstants(FrodoKEMMode mode) : m_mode(mode), m_len_a(128), m_n_bar(8) { | ||
#if !defined(BOTAN_HAS_AES) | ||
BOTAN_ARG_CHECK(!mode.is_aes(), "cannot instantiate AES-based FrodoKEM: This build does not support AES"); | ||
#endif |
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.
Instead of having to do a macro check along with mode.is_aes()
let's add
mode.is_available()
(or is_supported
or ...)
and use it here, in speed.cpp
, and anywhere else we're doing an explicit BOTAN_HAS_AES
check.
This also generalizes if we decide to make SHAKE modes optional in the future.
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 added this method and used it when possible, but in some instances (row generation, KATs, and PK_Key_Generation_Test
) compile time checks were still needed.
src/tests/test_frodokem.cpp
Outdated
Botan::FrodoKEMMode::eFrodoKEM1344_AES, Botan::FrodoKEMMode::eFrodoKEM976_AES, | ||
Botan::FrodoKEMMode::eFrodoKEM640_AES, Botan::FrodoKEMMode::FrodoKEM1344_AES, | ||
Botan::FrodoKEMMode::FrodoKEM976_AES, Botan::FrodoKEMMode::FrodoKEM640_AES | ||
#endif |
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.
Instead of excluding the modes at compile time in this way lets just continue
if !mode.is_supported()
src/lib/pubkey/frodokem/frodokem.cpp
Outdated
} | ||
|
||
OID FrodoKEM_PublicKey::object_identifier() const { | ||
return OID::from_string(m_public->constants().mode().to_string()); |
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.
Couldn't this use mode().object_identifier()
?
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.
Fixed
src/lib/pubkey/frodokem/frodokem.cpp
Outdated
std::tuple(consts.n_bar(), consts.n()), | ||
shake.output<FrodoSampleR>(sizeof(uint16_t) * consts.n_bar() * consts.n())); | ||
|
||
const auto e_p = |
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 have here three repetitions of
FrodoMatrix::sample(consts,
std::tuple(consts.n_bar(), consts.XXX()),
shake.output<FrodoSampleR>(sizeof(uint16_t) * consts.n_bar() * consts.XXX())
I'm not sure if this is the best approach but: at the minimum it seems like a lambda that captures shake
as a mutable reference and takes XXX
(either n_bar
or n
) as an argument would clean this up.
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.
There are three similar invocations during decrypt - maybe a sample
helper on FrodoMatrix
or a function in an anon namespace in this file would be better than a lambda which would still have to be duplicated.
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.
Added a helper function to FrodoMatrix
. This nicely reduces complexity, thanks for the tip!
src/lib/pubkey/frodokem/frodokem.cpp
Outdated
b_pp.reduce(consts); | ||
c_p.reduce(consts); | ||
const bool cmp_b_p_b_pp = b_p.constant_time_compare(b_pp); | ||
const bool cmp_c_c_p = c.constant_time_compare(c_p); |
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.
These constant_time_compare
calls should probably return CT::Mask
instead of bools
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.
Agreed, thanks for pointing this out!
conditional_copy_mem
now also directly uses the mask instead of the value.
#include <botan/mem_ops.h> | ||
#if defined(BOTAN_HAS_AES) | ||
#include <botan/internal/aes.h> | ||
#endif |
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.
Larger question: is there really a reason
a) To include the AES support at all? It seems like everyone is moving to SHAKE for pqc
b) If AES support is required/useful (for standards conformance or contract reasons, etc): can we just make the AES support also mandatory? 99% of the time you want AES anyway for other reasons, and while you maybe don't need Frodo-AES, in the current approach there isn't any way to disable that anyway; if you enable plain old AES, plus Frodo, you get Frodo-AES. This contrasts with how Kyber was done where Kyber-90s is a distinct module that can be toggled off.
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.
To include the AES support at all?
In contrast to the NIST drafts, currently the ISO draft for (e)FrodoKEM explicitly specifies the AES variant. Otherwise we would have left it out.
This contrasts with how Kyber was done where Kyber-90s is a distinct module that can be toggled off.
Initially, we planned to build two sub-modules for FrodoKEM as well. However, in contrast to Kyber, AES in FrodoKEM is somewhat an "add-on", as both FrodoKEM-SHAKE and FrodoKEM-AES depend on SHAKE. 😞 So FrodoKEM without SHAKE doesn't make sense while FrodoKEM without AES still works partially.
You make a fair point, though, that we currently cannot switch off AES-support in FrodoKEM if both frodokem
and aes
are enabled in the build.
Let us try to extract the AES portions into a very thin module, see how much complexity that adds and then decide whether its worth it or not.
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.
Let us try to extract the AES portions into a very thin module, see how much complexity that adds and then decide whether its worth it or not.
Done here: 0694f1d
In my opinion the AES sub module doesn't bring much added complexity for giving users the flexibility to switch it off.
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 it's nice to be able to switch of FrodoKEM-AES even if you have the AES module.
What do you think about a FrodoKEM-SHAKE module?
It does not save any compile complexity but imo a minimized build with --enable-modules=frodokem_shake
for FrodoKEM-SHAKE, --enable-modules=frodokem_aes
for FrodoKEM-AES, and --enable-modules=frodokem
for both variants would be clearer than the current solution. It also covers the case where one would want to disallow FrodoKEM-SHAKE. I know we'd prefer always using FrodoKEM-SHAKE but at least according to the current ISO specification both variants are equal, so one would expect to get both with the "FrodoKEM" module and not with the "FrodoKEM-AES" module.
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.
For the sake of consistency, we could certainly do that. Note that you will need to explicitly enable frodokem_shake
and frodokem_aes
. There's (AFAIK) no concept of a "virtual" module feature that would enable both variants. Similarly, kyber
enables the Keccak-based Kyber and kyber_90s
enables the AES variant. There's an (internal) kyber_common
with the base implementation that both flavors depend on.
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 applied this suggestion. Now there are three modules:
frodokem_common
(internal module) with the implementation,frodokem_aes
the AES-based matrix generator (depends onaes
andfrodokem_common
), andfrodokem
the SHAKE-based matrix generator (depends onshake_xof
andfrodokem_common
)
It is possible to enable either frodokem_aes
or frodokem
individually or both at the same time.
Thanks for the review! We now addressed all comments except the discussions on the modules and whether to skip KATs without AES support. |
Rebased to latest |
Rebased again, incorporated (the now closed) #3808. |
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.
LGTM, left a few style comments on bits that are probably leftover from the C
const size_t nwords = (constants.n_bar() * constants.n_bar()) / 8; | ||
uint16_t temp; | ||
uint16_t maskex = static_cast<uint16_t>(1 << constants.b()) - 1; | ||
uint16_t maskq = static_cast<uint16_t>(1 << constants.d()) - 1; |
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 think these two can both be const
|
||
size_t index = 0; | ||
for(size_t i = 0; i < nwords; i++) { | ||
templong = 0; |
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.
templong
should be declared here rather than being function scope.
for(size_t i = 0; i < nwords; i++) { | ||
templong = 0; | ||
for(size_t j = 0; j < 8; j++) { | ||
temp = static_cast<uint16_t>(((m_elements.at(index) & maskq) + (1 << (constants.d() - constants.b() - 1))) >> |
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.
temp
could be declared/initialized here and not be function scope
uint8_t b = 0; // bits in out[i] already filled in | ||
while(b < lsb) { | ||
const uint8_t nbits = std::min(static_cast<uint8_t>(lsb - b), bits); | ||
uint16_t mask = static_cast<uint16_t>(1 << nbits) - 1; |
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.
mask
can be const
right?
auto& current = elements.at(i * constants.n_bar() + j); | ||
current = 0; | ||
for(size_t k = 0; k < constants.n(); ++k) { | ||
current += static_cast<uint16_t>( |
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 expression is very hard to read thanks to how the inner comments get formatted. Splitting it out to a couple of variables with leading comments would probably help a lot, eg
const uint32_t b_ink = b.elements_at(...);
// Since ...
const uint32_t s_jnk = s.elements_at(...);
current += ...
for(size_t i = 0; i < n; ++i) { | ||
uint32_t sample = 0; // Avoid integral promotion | ||
uint16_t prnd = elements.at(i) >> 1; // Drop the least significant bit | ||
uint16_t sign = elements.at(i) & 0x1; // Pick the least significant bit |
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.
These two uint16_t
can be const
right?
Integrated into CLI benchmarking tool and X.509 tests Co-Authored-By: René Meusel <[email protected]>
Co-Authored-By: René Meusel <[email protected]>
Thanks, @randombit! We addressed the review comments. Currently, we're looking deeper into the application of the newly added endian load/store mechanics in FrodoKEM. But maybe we should leave these for after the Botan 3.3.0 release. What do you think? |
Co-Authored-By: Fabian Albert <[email protected]>
I'm quite certain we want to keep those refactorings for after the release. They require parts of #3869 which we certainly don't want to merge just before the release. Let's revisit this 3.3.0 is out. |
Closes #3106 by introducing FrodoKEM as specified in the ISO proposal.
Open Questions:
Future Work:
key_len
. It would be possible to load FrodoKEM keys with an additional parameter specifying the parameter set. However, it may be more worthwhile to provide a generalbotan_privkey_load
interface, similar tobotan_privkey_create
.