Repository navigation
SDitH3 Integration - #2586
SDitH3 Integration#2586owaldron wants to merge 10 commits into
Conversation
|
hi @owaldron ! If the OR is ready for review, should we mark it as so? so people know and can start to go through it, even though they are certain build failures that we have to solve. |
a8b4ac4 to
f7933e1
Compare
|
Hi @RodriM11, yup! I just marked it as ready for review |
|
@Frauschi 4 out of the remaining 5 build errors are related to Zephyr, would you mind taking a quick look to see if anything is amiss? |
|
For the timed-out noble-memopt test, I opened a PR #2595 that should speedup CI enough to avoid needing to arise the timeout cap. |
|
I dug into the failing Zephyr jobs. There are two problems stacked on top of each other. Only cortex-a53 and riscv64 fail because SDitH is 64-bit only, and the KEMs sample breaks too because the Zephyr module has no switch for SDitH, so it gets built into every sample. 1. Zephyr builds with Changing it to 2. Signing needs way more heap than the sample has With that fixed, the Signatures sample fails at runtime with Suggestion I'd add a Kconfig switch in plus the usual block in if(CONFIG_LIBOQS_ENABLE_SIG_SDITH)
set(OQS_ENABLE_SIG_SDITH ON)
else()
set(OQS_ENABLE_SIG_SDITH OFF)
endif()That should get the Zephyr jobs green. If you want SDitH covered in CI too, add |
20260928 Code review with OwenTransparent AES key schedule memory layoutSDitH treats AES key schedule struct as transprent There are different level of shimming we could do:
Explore the difficulty of doing each option. OSSL shimmingWith respect to providing an alternative API for writing AES key schedule to |
Following the suggestions from the comment open-quantum-safe#2586 (comment) to configure zephyr for SDitH. This disables SDitH on zephyr by default, since its large heap usage goes OOM on the CI boards. Co-authored-by: Tobias Frauenschläger <t.frauenschlaeger@me.com> Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Following the suggestions from the comment open-quantum-safe#2586 (comment) to configure zephyr for SDitH. This disables SDitH on zephyr by default, since its large heap usage goes OOM on the CI boards, and restricts it to 64-bit targets. Co-authored-by: Tobias Frauenschläger <t.frauenschlaeger@me.com> Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
56a7f06 to
c6b3bc4
Compare
Added a new function `OQS_AES128_ECB_rekey` to the liboqs AES API, which allows re-keying an existing AES key schedule without needing to allocate a new one. This is particularly useful for SDitH which performs multiple encryptions under different keys in series. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Allow a common dependency to declare a `common_dep` of its own, so that a commons can build on each other. A common that only aggregates other commons in this way no longer has to declare `sources`. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Points copy_from_upstream to the SDitH integration branch (still in fork) and adds the resulting generated code. Everything in this commit outside scripts/copy_from_upstream/ is machine-generated and is not intended to be reviewed by hand. Reviewers wanting to see what was changed relative to upstream SDitH should read those at the fork rather than the generated tree here. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
SDitH's reference code uses __uint128_t and GCC/Clang function attributes, neither of which MSVC provides, so the family is disabled on Windows and on any 32-bit target. There is also a little-endian assumption in the code, so it is also restricted to little-endian targets. The imported code is also not yet free of compiler warnings, so its targets have COMPILE_WARNING_AS_ERROR turned off to keep those warnings from blocking CI. This is tracked by open-quantum-safe#2588 and should be removed once the upstream code is cleaned up. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Registers the twelve SDitH3 schemes in the constant-time issue list and adds nine valgrind suppressions for them. These need to be audited to ensure that they are indeed false positives. Tracked by open-quantum-safe#2609 Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Following the suggestions from the comment open-quantum-safe#2586 (comment) to configure zephyr for SDitH. This disables SDitH on zephyr by default, since its large heap usage goes OOM on the CI boards, and restricts it to 64-bit targets. Co-authored-by: Tobias Frauenschläger <t.frauenschlaeger@me.com> Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Added `sdith.yml` to the algorithm docs to generate documentation, and listed the family in the README and ALGORITHMS tables. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
c6b3bc4 to
ec5eef8
Compare
| } | ||
| } | ||
| }, | ||
| { |
There was a problem hiding this comment.
On this I've got a question: Does this file define liboqs as the entity responsible for this crypto? There are companies using S/CBOMs as a way to document their products have a "maintained" software stack and I'd rather not have their products containing SDitH point at us but rather at the upstream. That concern may not be SDitH specific, so tagging @bhess for advice as the original adder of this bookkeeping facility (hoping I remember this right?).
|
|
||
| static void AES128_ECB_rekey(const uint8_t *key, void *schedule) { | ||
| struct key_schedule *ks = (struct key_schedule *) schedule; | ||
| // cipher MUST be NULL to avoid the reset at openssl/evp_enc.c:174 |
There was a problem hiding this comment.
This reads very weird: a) is this true? b) This at most is a moment-in-time statement changing whenever OpenSSL source code changes.
There was a problem hiding this comment.
I admit that this is weird, and is one of my biggest points of uncertainty with this PR.
a)
This option was suggested to me by Claude when brainstorming, but I've looked into it enough to convince myself that it is true. EVP_EncryptInit_ex wraps EVP_CipherInit_ex which itself wraps evp_cipher_init_internal. Within this function, the reset calls are all guarded with cipher != NULL (source):
/* Ensure a context left lying around from last time is cleared */
if (cipher != NULL && ctx->cipher != NULL) {
unsigned long flags = ctx->flags;
EVP_CIPHER_CTX_reset(ctx);
/* Restore encrypt and flags */
ctx->encrypt = enc;
ctx->flags = flags;
}The best evidence that this is safe behaviour is the unit test test_evp_reset which shows that passing NULL to all but the key slot re-keys the ctx (source):
if (!TEST_true(EVP_CipherInit_ex(ctx, type, NULL, NULL, NULL, t->initenc))) {
errmsg = "EMPTY_ENC_INIT";
goto err;
}
if (!TEST_true(EVP_CIPHER_CTX_set_padding(ctx, 0))) {
errmsg = "PADDING";
goto err;
}
if (t->keyfirst && !TEST_true(EVP_CipherInit_ex(ctx, NULL, NULL, t->key, NULL, -1))) {
errmsg = "KEY_INIT (before iv)";
goto err;
}
if (!evp_init_seq_set_iv(ctx, t)) {
errmsg = "IV_INIT";
goto err;
}
if (t->keyfirst == 0 && !TEST_true(EVP_CipherInit_ex(ctx, NULL, NULL, t->key, NULL, -1))) {
errmsg = "KEY_INIT (after iv)";
goto err;
}although this test unfortunately doesn't replace the existing key with a new key, nor does it assert that the context isn't free'd & re-allocated.
The lack of documentation on this use case does leave me a bit uneasy, however OpenSSL doesn't seem to document these functions much at all to begin with. The closest thing I could find documenting this behaviour was a (very old) comment regarding similar (but separate) logic in the legacy path of the evp_cipher_init_internal function (source):
/*
* Whether it's nice or not, "Inits" can be used on "Final"'d contexts so
* this context may already have an ENGINE! Try to avoid releasing the
* previous handle, re-querying for an ENGINE, and having a
* reinitialisation, when it may all be unnecessary.
*/b)
If we want to avoid using this feature, or if openssl moves away from supporting this, we can always switch to a fresh ctx init for the OpenSSL backend only. In this case, there is no getting around the free/alloc cost for each re-key for OpenSSL, but we can still reap the performance rewards for the other backends.
There was a problem hiding this comment.
Thanks for the explanation @owaldron ! I had a hunch that it was @claude finding out about this (current state of things). And no contradiction as to OpenSSL's state of documentation :-/
The extremely cautious person in my would go for option b) to avoid "surprises down the road". The more enterprising person in me would use this (e.g., sub-optimal performance numbers) to touch base with the OpenSSL community to either commit on the a) behaviour (and document it) or confirm b) to be the safe choice. Your call as to how many folks you'd like to interact with. I can recommend it: The OpenSSL community is surely interesting, particularly its breadth.
There was a problem hiding this comment.
Good call, I left a question in OpenSSL's GitHub discussions Q&A (number 33169) asking if this is idiomatic and safe behaviour. Hopefully the community can help give some guidance on whether this use of EVP_CipherInit is supported and if it should be documented or tested anywhere.
There was a problem hiding this comment.
A helpful commenter answered the Q&A post (link) and claims this behaviour is intended, citing the man page as evidence.
Two additional points they brought up, and why I think they're already addressed:
check the return of every init call (a failed init leaves the ctx unusable until you init it again)
We currently wrap our ossl calls with OQS_OPENSSL_GUARD, which raises an error on a bad return code. Although, a comment does call this macro "a temporary workaround"... I assume this refers to #1456's aim to return a status code instead of raising an error.
on 3.x you might as well switch to EVP_EncryptInit_ex2 since the non-2 ones are marked legacy now
I think we continue to support OpenSSL 1.1.1 (?), so it doesn't yet make sense to switch to _ex2. This migration would follow a complete 1.1.1 deprication.
There was a problem hiding this comment.
This migration would follow a complete 1.1.1 deprication.
That actually is an issue worth while raising (imo executing on) given OpenSSL 1.1.1 has reached end of life 3 years ago.
| #include "sdith_namespace.h" | ||
| #endif | ||
|
|
||
| /* new round key = g ^ w ^ (w<<4) ^ (w<<8) ^ (w<<12), with g broadcast to all 4 words. |
There was a problem hiding this comment.
I assume all of this is lifted out of SDitH original code? Otherwise, this would warrant a serious review (that I'd not be competent enough for). General question: What will happen with this common code as/if SDitH does some logic/security changes upstream in this space? Will they be transferred here or will this code drift from the upstream?
There was a problem hiding this comment.
Yup, (other than the added #include) this is copied right from the sdith source (here).
In the ideal case, we should just have to update the commit hash in copy_from_upstream.yml and run copy_from_upstream to keep all of the sdith code up-to-date. Right now, since this points to a fork, there's the extra step of syncing the changes too. I think that the biggest issue with this is that upstream maintainers would have to notify us when changes occur, or come here to run the script themselves.
If PRs like #2594 continue to close the performance gap, perhaps we can consider shimming this optimized AES backend as well. For now though (even with this PR), I think that the upstream's optimized AES implementation is better suited for their use case than the liboqs API.
There was a problem hiding this comment.
come here to run the script themselves.
That would be the perfect end state: I we (you!) make it simple for the upstream to keep their code in sync with liboqs the project achieves what I like to call PMM (perfect mutual motivation): Users get an up-to-date implementation (maybe also fielding security or other usage concerns that way); the upstream gets user feedback to improve their creation; OQS facilitates this without picking up undue maintenance load.
I think that the upstream's optimized AES implementation is better suited for their use case than the liboqs API.
Sounds sensible, too. OQS shouldn't prescribe things to algorithms that weaken them. To the contrary, if an upstream can make a good case for a better "common code" base, OQS should consider adopting it, maybe also for other algs needing that code.
There was a problem hiding this comment.
That would be the perfect end state: I we (you!) make it simple for the upstream to keep their code in sync with liboqs the project achieves what I like to call PMM
Sound good! I'll look into opening a PR upstream soon to eliminate the one level of indirection for the maintainers.
OQS should consider adopting it, maybe also for other algs needing that code.
Given that MQOM, SDitH, and FAEST all use AES for similar purposes within the VOLE-in-the-Head framework, I've started to explore whether some enhancement to liboqs' common code could be leveraged by all three algorithms. I have yet to try anything yet, though
|
Hi @baentsch. Thank you for reviewing this so quickly! I'll work now to address your comments |
xuganyu96
left a comment
There was a problem hiding this comment.
liboqs's AES module provides a set of AES-related APIs. There are two primary
sets of backends: a native implementation and an OpenSSL implementation. The
two backends are respectively defined as callback structs in src/common/aes/aes_c.c
and src/common/aes/aes_ossl.c. At compile-time, depending on whether
OQS_USE_AES_OPENSSL is turned on, CMake will choose to compile aes_c.c or
aes_ossl.c.
The addition of a rekey API to liboqs's AES module is easy to implement in with
aes_c.c, but the OpenSSL backend is trickier to work with. OpenSSL 3.3's
documentation
lists the following example:
ctx = EVP_CIPHER_CTX_new();
if (!EVP_EncryptInit_ex2(ctx, EVP_idea_cbc(), key, iv, NULL)) {
/* Error */
EVP_CIPHER_CTX_free(ctx);
return 0;
}Where EVP_EncryptInit_ex/EVP_EncryptInit_ex2 takes the input encryption key
and updates the context, so subsequent calls to something like EVP_EncryptUpdate
will use the updated encryption key and its corresponding round keys. We cannot
find official documentation promising that "if certain parameters are passed in
as NULL, then EVP_EncryptInit_ex will refresh round keys without freeing the
existing context and allocating for a new context". There are only evidence in
the source code, in the unit tests, and in some discussion comments.
It's probably a better idea to stick with more officially documented OpenSSL
behaviors.
With untested behavior:
static void AES128_ECB_rekey(const uint8_t *key, void *schedule) {
struct key_schedule *ks = (struct key_schedule *) schedule;
OQS_OPENSSL_GUARD(OSSL_FUNC(EVP_EncryptInit_ex)(ks->ctx, NULL, NULL, key, NULL));
}With safer behavior:
static void AES128_ECB_rekey(const uint8_t *key, void *schedule) {
struct key_schedule *ks = (struct key_schedule *) schedule;
OSSL_FUNC(EVP_CIPHER_CTX_free)(ks->ctx);
ks->ctx = OSSL_FUNC(EVP_CIPHER_CTX_new)();
OQS_EXIT_IF_NULLPTR(ks->ctx, "OpenSSL");
OQS_OPENSSL_GUARD(OSSL_FUNC(EVP_EncryptInit_ex)(ks->ctx, oqs_aes_128_ecb(), NULL, key, NULL));
}| oqs_aes128_load_schedule_into_ni(key, *_schedule); | ||
| } | ||
|
|
||
| void oqs_aes128_rekey_ni(const uint8_t *key, void *_schedule) { |
There was a problem hiding this comment.
if oqs_aes128_rekey_ni is exactly oqs_aes128_load_schedule_into_ni, then there is no need to declare oqs_aes128_load_schedule_into_ni separately.
| } | ||
|
|
||
| void oqs_aes128_load_schedule_ni(const uint8_t *key, void **_schedule) { | ||
| *_schedule = OQS_MEM_malloc(sizeof(aes128ctx)); |
There was a problem hiding this comment.
Does OQS_MEM_malloc check for null pointer? If not, there should be a null pointer check right afterwards.
There was a problem hiding this comment.
Does
OQS_MEM_malloccheck for null pointer? If not, there should be a null pointer check right afterwards.
Good question. And there's even a macro for that (OQS_EXIT_IF_NULLPTR). Additional note to committers with too much time (or a willing AI :) at their hands: It may be worth while asking this question for the whole code base. And at least my AI tells me there's "room for improvement" (separate issue? quick PR?)
There was a problem hiding this comment.
Had some spare credits, so I let Claude whirl away at this quickly: #2633
|
Hi, From my experience as a developer of MQOM and integrating it into
Having said that, beware that reusing / integrating https://github.com/sdith/sdith/blob/ce7adf06476674d4ff990f378f83f9981cd6745b/lib/aes/aes128_ctrle_special_avx2.c APIs "as is" is somehow specific to SDitH:
I guess a useful common ground generic API would be:
For CTR based schemes such as FAEST and SDitH, the "counters" would be prepared by the scheme before calling the OQS AES API. Regards, |
|
Hi @rben-dev, thank you so much for your advice!
You're right, I just ran my own benchmarks for sdith on ARMv8 and AES-NI (without AVX2) and the speedup from rekeying is 0 to 2%. The exception is a 28% speedup when liboqs uses the OpenSSL backend, but as discussed above (here and here) we aren't certain this implementation is safe anyways. I played a bit with @claude and it showed that (if several complimentary code changes are made) rekeying could lead to a 5% speedup across all backends, but for now it seems like a premature optimization that perhaps might be best explored after the integration.
That's fair, but since I'm not on the SDitH team I did not want to make this decision on their behalf. Is MQOM's 10% to 15% performance loss between its standalone performance and it's liboqs integration? As you pointed out, SDitH interweaves the logic for their custom litte-endian CTR into their x2/x4/... AES in
I agree that this is probably the most useful API. It seems like a natural follow up to #2594. I'll look into adding this as a standalone change in a following PR, with part of the goal being to regaining as much of the MQOM performance gap as possible. |
|
Thanks for the feedback.
Seems interesting, and yes if 5% can be achieved that would be great and benefit to all the algorithms using AES.
This makes sense.
Yes, and this has been measured all things being equal except for the AES backend: MQOM's internal AES-NI backend versus MQOM's API delegating to
Not saying that this is the best way to design this, but on the MQOM side we have tried to split all the low-level stuff in abstraction layers enc.h that multiplexes enc_local.h, enc_liboqs.h and so on. All the x2/x4/x8 handling is delegated to this low-level layer (each low-level backend exposes such APIs, and for those unaccelerated x2/x4/x8 is simply a serialization of the unit operation). This leaves all the high-level operations on GGM trees free to use such APIs without knowing if the backend is accelerated or not, and fallback to "serialized" performance when parallelization is not available. Of course I understand that Anyways, thanks for your efforts on integrating SDitH, and trying to smooth other MPCitH candidates performance! Regards, |
Move upstream away from using AES rekey API, since it is being reverted. Also made some documentation changes, include the process for generating KAT checksums. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
This reverts commit db05751. Measurement showed the in-place rekey did not earn a new public AES API. On a mainstream x86-64 build it is worth nothing at all, since SDitH's AVX2 variants make no liboqs AES-128 calls and cat3/cat5 use Rijndael-256. On an arm64 cat1 reference build it saves ~5% of signing against the liboqs AES backend, where it only avoids a malloc and free per key schedule. The larger saving against the OpenSSL backend came from re-initializing an EVP_CIPHER_CTX in place, which liboqs rejected as unsafe. It is also dominated by the ARMv8 AESE key schedule and by a load_schedule_into-style API, neither of which needs it. Its only consumer, the SDitH AES glue, no longer calls it. Assisted-by: Claude:claude-opus-5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
Since the sdith integration code remains on a personal fork, this commit switches the upstream mechanims to be patches on top of the official repo's main. Assisted-by: Claude:claude-opus-5.5 Signed-off-by: Owen Waldron <owen.waldron@uwaterloo.ca>
This PR integrates SDitH's round 3 code into liboqs (#2453). The source upstream repo is at sdith/sdith, which was forked at owaldron:feat/liboqs-integration to house upstream changes required for integration (git diff).
The changes to the upstream repo are:
bbc96a6: Portability fixes for liboqs integrationb711691: liboqs metadata and wrapper API (no glue code)a0e7be2: FIPS202 shim onto liboqs' SHA3e47fad2: AES shim onto liboqs' AES, using the new rekey API7e68c20: Symbol namespacing (gen_namespace.sh+ generated header)And the changes to liboqs are:
acc8cdfb: AddOQS_AES128_ECB_rekeyto the AES API, with KATdb18c554: copy_from_upstream: nested common dependenciesca71f231: (Generated) SDitH3 import via copy_from_upstreamddaa6dd0: Platform gating and-Werrorexemption (Address-DOQS_STRICT_WARNINGS=ONwarnings for SDitH code #2588)fc8461e5: Constant-time suppressions (Verify SDitH3test_constant_timesuppressions #2609)49e261c6: Zephyr configurationc6b3bc4e: Algorithm documentationGenerative AI Disclosure: generative AI was used throughout the development process to aid with understanding, drafting, debugging, and reviewing code. Any and all code generated by AI was reviewed and possibly modified before inclusion. Non-trivial contributions by generative AI are accompanied by an
Assisted-bytag in the commit.