Skip to content

[crypto/mldsa] Cryptolib implementation of ML-DSA-87 sign#30676

Open
andrea-caforio wants to merge 6 commits into
lowRISC:masterfrom
andrea-caforio:mldsa87-sign-cryptolib
Open

[crypto/mldsa] Cryptolib implementation of ML-DSA-87 sign#30676
andrea-caforio wants to merge 6 commits into
lowRISC:masterfrom
andrea-caforio:mldsa87-sign-cryptolib

Conversation

@andrea-caforio

Copy link
Copy Markdown
Contributor

No description provided.

The computation of the message hash mu is common to both signature
generation and verification.

Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
@andrea-caforio andrea-caforio requested a review from a team as a code owner July 7, 2026 09:24
@andrea-caforio andrea-caforio requested review from etterli, h-filali, nasahlpa, siemen11 and timothytrippel and removed request for a team and timothytrippel July 7, 2026 09:24
@andrea-caforio andrea-caforio self-assigned this Jul 7, 2026
@andrea-caforio andrea-caforio added Type:Enhancement Feature requests, enhancements SW:cryptolib Crypto library labels Jul 7, 2026
@andrea-caforio andrea-caforio force-pushed the mldsa87-sign-cryptolib branch from 8a07c0e to dbfeb5e Compare July 7, 2026 09:38

@siemen11 siemen11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you @andrea-caforio ! Amazing to see the sign in action!

Comment thread sw/device/lib/crypto/impl/mldsa.c Outdated
ARRAYSIZE(rnd_data), kHardenedBoolTrue));
HARDENED_TRY(entropy_csrng_uninstantiate());

if (launder32(sign_mode) == kOtcryptoMldsaSignModeDet) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is a general comment, but given the security concern with deterministic ML-DSA. Shouldn't we maybe think of trying to tie this rnd to query it from EDN1 in the OTBN instead of going through Ibex?
If really needed you can write a magic value in OTBN to skip the edn1 pull of this rnd (since DMEM starts at 0), and our cycle count check can be a cross reference that OTBN skipped (or did not skip) this instantiation correctly as FI protection?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The problem is, how do we do the double-sign if we sample the randomness in OTBN? In fact, the current implementation is not correct either because we need to sample it at an even higher point such that it is the same randomness for both invocations of sign.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Isn't the randomness for rho'? Then the same rho' is used every sign loop?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Meaning, maybe we can place rho' in memory and skip its re-generation argh ok no redundancy for FI protection 🫠

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it would be good to re-execute the stuff before the rejection loop but reading out RHO' seems to be the only way currently we can achieve some kind of double-sign.

Comment thread sw/device/lib/crypto/impl/mldsa/mldsa.c Outdated
// Stall until the OTBN finishes.
HARDENED_TRY(otbn_busy_wait_for_done());

// Load c_tilde_prime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Putting this out there, but do we have some protection against an early exit by ML-DSA? Meaning a fault makes OTBN quit out early we get a rejected z and c_tilde back which can be used for secret key retrieval. The way I understand it is that our double sign protects against this, but we do not wipe DMEM between the two iterations, so a double early exit would actually be problematic.

Thinking on that, maybe we should specifically clear z between the two redundant calls?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why is it not cleared in between calls? Both invocations are wrapped in a HARDENED_TRY_WIPE_DMEM macro. But you're right, there is currently no other protection against early exits.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm I see

HARDENED_TRY(otcrypto_mldsa87_sign_async_start(private_key, message, context,
                                                 hash_mode, sign_mode, kappa));
HARDENED_TRY(otcrypto_mldsa87_sign_async_finalize(signature, &kappa));

For efficiency's sake I guess we do not want to wipe DMEM fully between redundant sign calls right?
But we can do a dmem write to set z back to zero?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this possible? Does DMEM persist even after the application exits? Is the invocation protocol different for the second run?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah it calls the otbn_load_app again, my misinterpretation sorry. I was thinking the redundancy to be in the internal driver not in the wrapper

* @param mu The resulting message hash (64 bytes).
* @return Result of the operation (OK or error).
*/
otcrypto_status_t compute_mu(const otcrypto_hash_digest_t *tr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for putting this in a function, very clean!

const otcrypto_const_byte_buf_t context,
otcrypto_mldsa_hash_mode_t hash_mode, otcrypto_word32_buf_t signature) {
// TODO: Connect ML-DSA operations to API.
return OTCRYPTO_NOT_IMPLEMENTED;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PQC is open for business 🥳

Add the abridged, random and deterministic signing modes to the
ML-DSA-87 sign OTBN app and restructure the DMEM locations to allows
for writing the key and reading the signature in single continuous
blobs to the OTBN.

Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
@andrea-caforio andrea-caforio force-pushed the mldsa87-sign-cryptolib branch from dbfeb5e to 1df54f2 Compare July 9, 2026 07:29
@andrea-caforio

Copy link
Copy Markdown
Contributor Author

It took me a while. :-) The OTBN app no has three modes: abridged, random, deterministic. Abridged mode (used for the second redundant sign call) skips the computation of RHO and setting the RND and assumes that the nonce KAPPA remains in DMEM. Random and deterministic modes are protected with an instruction counter. The second sign call is now part of the finalize function.

@siemen11 siemen11 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you @andrea-caforio ! Really nice!

// Buffer for the second signature value.
uint32_t sig_cmp_data[kMldsa87SigWords];

// Execute the second abridged sign and wait for its completion.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: you can mention in a comment that this also wipes DMEM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread sw/device/lib/crypto/impl/mldsa/mldsa.c Outdated
hardened_memeq(signature->data, sig_cmp_data, kMldsa87SigWords),
kHardenedBoolTrue);

return otbn_dmem_sec_wipe();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is better as a return OTCRYPTO_OK and wrap it with a HARDENED_TRY_WIPE_DMEM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

The FIPS-compliant implementation of the FIPS-204 signature generation
function with pure/pre-hash modes and random/deterministic signing. To
thwart FI attacks, sign is executed twice (only the successful
rejection loop in the second run).

Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
This commit includes a functest for pure and pre-hash mode.

Signed-off-by: Andrea Caforio <andrea.caforio@lowrisc.org>
@andrea-caforio andrea-caforio force-pushed the mldsa87-sign-cryptolib branch from 1df54f2 to c9da807 Compare July 9, 2026 14:46
@andrea-caforio andrea-caforio added the CI:Rerun Rerun failed CI jobs label Jul 9, 2026
@github-actions github-actions Bot removed the CI:Rerun Rerun failed CI jobs label Jul 9, 2026

@nasahlpa nasahlpa left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Amazing work, thanks!

@johannheyszl

Copy link
Copy Markdown
Contributor

great!

from our discussion, please let's have the second sign configurable per key security level

@andrea-caforio

Copy link
Copy Markdown
Contributor Author

great!

from our discussion, please let's have the second sign configurable per key security level

The problem is we don't pass the key (which contains the security level parameter) to finalize functions in the cryptolib, where the second sign is executed. Maybe we can leave it on by default for the moment and later reassess it?

@johannheyszl

Copy link
Copy Markdown
Contributor

is there no precedence from ECDSA?

@andrea-caforio

Copy link
Copy Markdown
Contributor Author

is there no precedence from ECDSA?

Not sure, I can't find an instance where a key is passed to a finalize function.

@nasahlpa

Copy link
Copy Markdown
Contributor

We would have two options:

  1. Extend the finalize API such that it takes the key and only uses the security level from it.

  2. For ECDSA we have the same problem, so we offer the following function:

    otcrypto_status_t otcrypto_ecdsa_p256_sign_verify(

    Here, we loose the capability of the async interface. Not sure if this is wanted.

My suggestion would be to keep it as it is (ie always do the double sign) and if integrators need a faster solution without double sign we could follow option 1 from above.

@siemen11

Copy link
Copy Markdown
Contributor

We would have two options:

  1. Extend the finalize API such that it takes the key and only uses the security level from it.

  2. For ECDSA we have the same problem, so we offer the following function:

    otcrypto_status_t otcrypto_ecdsa_p256_sign_verify(

    Here, we loose the capability of the async interface. Not sure if this is wanted.

My suggestion would be to keep it as it is (ie always do the double sign) and if integrators need a faster solution without double sign we could follow option 1 from above.

Would we maybe just offer both API calls? double_sign_finalize and sign_finalize?

@nasahlpa

Copy link
Copy Markdown
Contributor

We would have two options:

  1. Extend the finalize API such that it takes the key and only uses the security level from it.

  2. For ECDSA we have the same problem, so we offer the following function:

    otcrypto_status_t otcrypto_ecdsa_p256_sign_verify(

    Here, we loose the capability of the async interface. Not sure if this is wanted.

My suggestion would be to keep it as it is (ie always do the double sign) and if integrators need a faster solution without double sign we could follow option 1 from above.

Would we maybe just offer both API calls? double_sign_finalize and sign_finalize?

Then we have the problem with code size overhead. Due to how the double sign works, we can basically not reuse much code of sign_finalize in double_sign_finalize. Not a big issue though.

@siemen11

Copy link
Copy Markdown
Contributor

We would have two options:

  1. Extend the finalize API such that it takes the key and only uses the security level from it.

  2. For ECDSA we have the same problem, so we offer the following function:

    otcrypto_status_t otcrypto_ecdsa_p256_sign_verify(

    Here, we loose the capability of the async interface. Not sure if this is wanted.

My suggestion would be to keep it as it is (ie always do the double sign) and if integrators need a faster solution without double sign we could follow option 1 from above.

Would we maybe just offer both API calls? double_sign_finalize and sign_finalize?

Then we have the problem with code size overhead. Due to how the double sign works, we can basically not reuse much code of sign_finalize in double_sign_finalize. Not a big issue though.

Hmm, that sounds unlikely, it feels that a couple of well named subroutines can cover this quite well. One regular sign finalize call and then some wrapper to bridge the new instance of the sign without cleaning DMEM entirely, and recalling the sign

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

SW:cryptolib Crypto library Type:Enhancement Feature requests, enhancements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants