Skip to content
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: Classic McEliece #3883

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

FAlbertDev
Copy link
Collaborator

@FAlbertDev FAlbertDev commented Jan 11, 2024

This PR relates to the Classic McEliece KEM as specified in this ISO draft. It also contains the instances defined in the NIST Round 4 submission. The test cases were generated using the NIST submissions reference implementation. Note that Classic McEliece (module cmce) is not the original McEliece Algorithm that is implemented in Botan's mce module. See the Classic McEliece homepage, for a brief comparison.

TODO Tracker

@coveralls
Copy link

coveralls commented Jan 11, 2024

Coverage Status

coverage: 92.281% (+0.5%) from 91.83%
when pulling 0dcbeac on Rohde-Schwarz:pqc/classic_mceliece
into 00e234d on randombit:master.

@reneme reneme added this to the Botan 3.4.0 milestone Jan 11, 2024
@FAlbertDev FAlbertDev force-pushed the pqc/classic_mceliece branch 4 times, most recently from f73829f to e505390 Compare January 19, 2024 14:53
@atreiber94 atreiber94 force-pushed the pqc/classic_mceliece branch 2 times, most recently from 04b9314 to 190261d Compare January 19, 2024 15:59
@reneme reneme self-requested a review January 22, 2024 09:33
@FAlbertDev

This comment was marked as resolved.

@reneme reneme force-pushed the pqc/classic_mceliece branch 6 times, most recently from 7662592 to 2a3e60f Compare January 23, 2024 15:55
@reneme

This comment was marked as resolved.

Copy link
Collaborator

@reneme reneme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mostly looks at cmce_decaps.cpp, cmce_encaps.cpp and cmce.cpp, noting many minor code style things and a few suggestions for alternative code structuring. Not looking into the Classic McEliece specifics at all here.

src/build-data/oids.txt Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_types.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_encaps.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_encaps.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_encaps.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_encaps.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_encaps.cpp Outdated Show resolved Hide resolved
src/lib/utils/strong_type.h Outdated Show resolved Hide resolved
Copy link
Collaborator

@reneme reneme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass on cmce_field_orderings.cpp. I'm somewhat concerned that this isn't very efficient. But it may well be "good enough". Should we profile that?

src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
Copy link
Collaborator

@reneme reneme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More comments. I didn't look at cmce_parameter_set.*, cmce_parameters_* and cmce_poly.*, yet.

src/lib/pubkey/classic_mceliece/cmce_keys_internal.cpp Outdated Show resolved Hide resolved
src/lib/utils/strong_type.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce.cpp Outdated Show resolved Hide resolved
Comment on lines 41 to 49
// TODO: Only for test instances. Remove on final PR
size_t m = Classic_McEliece_GF::log_q_from_mod(mod);

for(int i = static_cast<int>(m) - 2; i >= 0; --i) {
x ^= CT::Mask<uint32_t>::expand((uint32_t(1) << (i + m)) & x)
.if_set_return(static_cast<uint32_t>(mod.get()) << i);
}

return GF_Elem(static_cast<uint16_t>(x));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to remove (and probably replace by some exception?)

src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_matrix.cpp Outdated Show resolved Hide resolved
Comment on lines 247 to 258
Code_Word Classic_McEliece_Matrix::mul(const Classic_McEliece_Parameters& params, const Error_Vector& e) const {
auto s = e.subvector(0, params.pk_no_rows());
auto e_T = e.subvector(params.pk_no_rows());
auto pk_slicer = BufferSlicer(m_mat_bytes);

for(size_t i = 0; i < params.pk_no_rows(); ++i) {
auto pk_current_bytes = pk_slicer.take(params.pk_row_size_bytes());
auto row = secure_bitvector(pk_current_bytes, params.n() - params.pk_no_rows());
row &= e_T;
s.at(i) = s.at(i) ^ row.has_odd_hamming_weight();
}

BOTAN_ASSERT_NOMSG(pk_slicer.empty());
return s.as<Code_Word>();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This produces quite a few copies and allocations. If that's in the implementation's hot path, we should probably want to have another look. Here's which (I believe cause allocations/copies):

  1. .subvector() always creates a new bitvector and copies the content into a newly allocated buffer,
  2. the c'tor of bitvector copies the passed-in buffer (particularly pk_current_bytes
  3. .as<> produces a copy of the bitvector with a new type

For (3): This could already be the right type using e.subvector<Code_Word>(), no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some context: This function is called once per encapsulation. Also, e and s are pretty small, while m_mat_bytes is gigantic. Therefore, 1) and 3) are not critical; I'll apply your suggestion for 3) anyway. I don't know how we can prevent 2) without having something like a bitvector view or dropping the bitvector altogether.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll have a timeboxed look into a bitvector that doesn't own its underlying storage. Perhaps that isn't too hard to achieve, especially when we can limit it to byte-aligned subvectors.

Copy link
Collaborator

@reneme reneme left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with a first pass on the implementation. Don't get put off by the number of comments. Most are just C++ style nits and smaller programming suggestions.

That's really good work! 😃

Comment on lines 48 to 57
/// Reduced instances for side channel analysis (Self-created test instance with
/// m=8, n=128, t=8, f(z)=z^8+z^7+z^2+z+1, F(y)=y^8+y^4+y^3+y^2+1)
/// Minimal instance without semi-systematic matrix creation and no plaintext confirmation
test,
/// Minimal instance with semi-systematic matrix creation and no plaintext confirmation
testf,
/// Minimal instance without semi-systematic matrix creation and with plaintext confirmation
testpc,
/// Minimal instance with semi-systematic matrix creation and with plaintext confirmation
testpcf
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: remove before merging

src/lib/pubkey/classic_mceliece/cmce_parameter_set.h Outdated Show resolved Hide resolved
Comment on lines 27 to 32
/**
* @returns ceil(n/d)
* TODO: Remove once LMS is merged
*/
constexpr size_t ceil_div(size_t n, size_t d) {
return (n + d - 1) / d;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-evaluate before merging this.

src/lib/pubkey/classic_mceliece/cmce_parameters.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_parameters.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_poly.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_poly.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_poly.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/info.txt Outdated Show resolved Hide resolved
src/lib/utils/bit_ops.h Outdated Show resolved Hide resolved
@FAlbertDev
Copy link
Collaborator Author

Thanks a lot for your extensive review, @reneme! I addressed your suggestions and am optimistic that this PR is ready to drop its Draft status 🎉. Note that some suggestions that depend on other PRs are still open, which are not critical, though. Also, note that an extensive side-channel analysis is still in progress.

@FAlbertDev FAlbertDev marked this pull request as ready for review February 5, 2024 14:49
constexpr bitref& operator^=(bool other) noexcept { return assign(this->is_set() ^ other); }

private:
constexpr bitref& assign(bool bit) noexcept { return (bit) ? set() : unset(); }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, DATA identified this line as a leakage during our SCA review.

Due to the ? operator, a control-flow difference is observed based on the boolean input bit variable.

The assign() routine is used within the push_back() routine, which in turn is used within the decode() routine. This may allow an adversary to observe the error vector e and, hence recover the shared secret.

We suggest to perform both the set() and unset() functions with the input as a mask, for example:

         private:
            constexpr bitref& assign(bool bit) noexcept {
                const block_type assign_mask = 0 - static_cast<block_type>(bit);
                this->m_block |=  (this->m_mask &  assign_mask);
                this->m_block &= ~(this->m_mask & ~assign_mask);
                return *this;
            }

In our case, this results in the following instructions - without any conditional branch based on the input:

[ ... ] 
                                         const block_type assign_mask = 0 - static_cast<block_type>(bit);
41d397: 41 f7 dc                   neg %r12d
[ ... ]
                                         this->m_block |= (this->m_mask & assign_mask);
41d3ab: 44 89 e1                   mov %r12d,%ecx
41d3ae: 21 c1                      and %eax,%ecx
                                         this->m_block &= ~(this->m_mask & ~assign_mask);
41d3b0: f7 d0                      not %eax
                                         this->m_block |= (this->m_mask & assign_mask);
41d3b2: 0a 0a                      or (%rdx),%cl
                                         this->m_block &= ~(this->m_mask & ~assign_mask);
41d3b4: 44 09 e0                   or %r12d,%eax
41d3b7: 21 c8                      and %ecx,%eax
[ ... ]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your analysis and your report! I applied your fix using Botan's constant-time helper class (7a0fe59)

@@ -1,6 +1,8 @@
/*
* An abstraction for an arbitrarily large bitvector that can
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reneme the code still contains some artefacts for varying the underlying data type that we'd ideally want to get rid of.

Copy link
Owner

@randombit randombit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have not done a full review yet, leaving some initial comments

x = (x + (x >> 4)) & 0xF0F0F0F0F0F0F0F;
return (x * 0x101010101010101) >> 56;
} else {
static_assert(!std::unsigned_integral<T>, "T is not a suitable unsigned integer value");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t understand this static_assert - shouldn’t the concept check on T already prevent this? It feels like this is intended to catch the case where T > 64 bits but that doesn’t necesarily apply if the compiler considers a 128 bit integer type to be unsigned_integral (maybe C++20 prohibits this, idk)

Perhaps instead starting the function with

static_assert(sizeof(T) <= 8, “T is not …”)

which anyway seems like a more clear statement of requirement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right. @reneme?

src/tests/runner/test_runner.cpp Outdated Show resolved Hide resolved
}

size_t Classic_McEliece_PublicKey::key_length() const {
return m_public->matrix().bytes().size();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a) Is it really necessary to encode the matrix (with memory allocation etc) just to determine the key length

b) Is the byte size of the matrix really the best value to return here? Returning the integer encoding of {n}{t} would be just as meaningul (ie, an arbitrary integer that goes upwards as the key gets stronger) while allowing some plausible method of mapping it back to a parameter set.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a) The matrix object stores the matrix bytes as a member. The method bytes only returns a const reference to these bytes. No new memory is allocated here.
b) Yeah, I got confused by the meaning of the method key length. I assumed it was the size of the public key in bytes without looking into the description. Thanks for mentioning this.
I think the most sensible value is $k = n - mt$ which is the dimension of the goppa code (i.e., there are $2^k$ code words).

src/lib/pubkey/classic_mceliece/cmce_parameter_set.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce.cpp Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce.h Outdated Show resolved Hide resolved
src/lib/pubkey/classic_mceliece/cmce_field_ordering.cpp Outdated Show resolved Hide resolved
@FAlbertDev
Copy link
Collaborator Author

Thanks very much for your initial review 🚀 I'll look into it.

@FAlbertDev
Copy link
Collaborator Author

I rebased to master (which removes the --no-stdout flag commit).

@FAlbertDev FAlbertDev force-pushed the pqc/classic_mceliece branch 2 times, most recently from 9a5ad0b to f6333fd Compare March 27, 2024 13:10
@reneme
Copy link
Collaborator

reneme commented Mar 28, 2024

Lets have another look at the tests. Especially the coverage build time suffers quite a bit from the new tests. Presumably, because it runs an extended test set. This is summing up to a few minutes, though.

image

@FAlbertDev
Copy link
Collaborator Author

FAlbertDev commented Apr 2, 2024

Especially, the coverage build time suffers quite a bit from the new tests.

Yeah. --run-long-tests tests all tests with all instances. If you think this is problematic, here are some suggestions to avoid this problem:

  1. We may only want to test some instances in the keygen tests. The correctness of the keys is already covered in the KAT tests; only the Botan interface is tested, which does not differ for various instances. Also, the Keygen test creates two keys instead of one (for KAT tests). This should reduce the total time by around 66%.
  2. Since the private and public keys are the same for pc and non-pc instances, we can try to "reuse" the key and save a keygen operation. This, however, is messy since we do not provide an interface for this reinterpretation, and the generic tests do not support something like this.
  3. We can only test pc instances and a few non-pc instances.

IMHO, running --run-long-tests tests that even take minutes should be allowed. I'm totally fine applying suggestion 1 since this is a low-hanging fruit without losing KATs. I prefer to keep all KAT instances and do not want to mess around as in 2.

@FAlbertDev
Copy link
Collaborator Author

Rebased to master + commit with some left-over suggestions.

@FAlbertDev
Copy link
Collaborator Author

FAlbertDev commented Apr 2, 2024

  1. We may only want to test some instances in the keygen tests. The correctness of the keys is already covered in the KAT tests; only the Botan interface is tested, which does not differ for various instances. Also, the Keygen test creates two keys instead of one (for KAT tests). This should reduce the total time by around 66%.

I applied this and decreased the total time for CMCE long tests from 81 sec to 17 sec.

@reneme
Copy link
Collaborator

reneme commented Apr 2, 2024

I applied this and decreased the total time for CMCE long tests from 81 sec to 17 sec.

Thanks!

IMHO, running --run-long-tests tests that even take minutes should be allowed.

I do agree, but then we should move away from running such tests for every push (like we actually do in the "sanitizer" and "coverage" builds, currently). If we wanted to have such long-running tests, we should outsource them to a nightly, to keep CI times manageable for interactive use cases.

@reneme
Copy link
Collaborator

reneme commented May 24, 2024

@FAlbertDev This needs another rebase. Also, with #3985 merged, it'll need to implement the new method Public_Key::raw_public_key_bits() as well as the method PK_Key_Generation_Test::public_key_from_raw() in the CMCE_Generic_Keygen_Tests test case.

FAlbertDev and others added 6 commits May 27, 2024 11:23
- constant time conditional swap with mask
- floor_log2

Co-Authored-By: Amos Treiber <amos.treiber@rohde-schwarz.com>
This is an implementation of the Classic McEliece KEM according to the
NIST Round 4 submission and the ISO draft 20230419.

Co-Authored-By: Amos Treiber <amos.treiber@rohde-schwarz.com>
@FAlbertDev
Copy link
Collaborator Author

@FAlbertDev This needs another rebase. Also, with #3985 merged, it'll need to implement the new method Public_Key::raw_public_key_bits() as well as the method PK_Key_Generation_Test::public_key_from_raw() in the CMCE_Generic_Keygen_Tests test case.

Done! @reneme Do you still want to reduce the test times further for the --run-long-test? Without a --nightly test CLI option or something similar, I see no elegant way to do so without cutting out KATs. I don't know if we want to introduce multiple layers of long tests, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants