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

Make ML-DSA signature decoding follow the spec #895

Merged
merged 5 commits into from
Feb 12, 2025

Conversation

bifurcation
Copy link
Contributor

Fixes #894

This PR also adds a test that runs many random signatures. I confirmed locally that this produced a failure rate of 0.4-1.1% for ML-DSA-65 and ML-DSA-87, all due to signature decoding, and all for the reasons discussed below. I never saw any failures for ML-DSA-44, but that seems plausible: The failure condition arises when the high-order bits of the r vector don't change, and the threshold $\gamma_2 / 2$ is roughly a factor of 2.75 lower for ML-DSA-44 than the other variants.

It appears that the root of the problem is a one-character fix, allowing repeated values in the monotonicity check in hint.rs. This makes it so that the tests usually pass. Marking this PR as draft because that same test fails intermittently, reporting internal error: entered unreachable code at hint.rs:30. I'll take a look at that tomorrow.

@bifurcation
Copy link
Contributor Author

Heh, and apparently I have some clippy clean-up to do as well!

Comment on lines 903 to 932
fn many_round_trip_test<P>()
where
P: MlDsaParams,
{
use rand::Rng;

const ITERATIONS: usize = 1000;

let mut rng = rand::thread_rng();
let mut seed = B32::default();

for _i in 0..ITERATIONS {
let seed_data: &mut [u8] = seed.as_mut();
rng.fill(seed_data);

let kp = P::key_gen_internal(&seed);
let sk = kp.signing_key;
let vk = kp.verifying_key;

let M = b"Hello world";
let rnd = Array([0u8; 32]);
let sig = sk.sign_internal(&[M], &rnd);

let sig_enc = sig.encode();
let sig_dec = Signature::<P>::decode(&sig_enc).unwrap();

assert_eq!(sig_dec, sig);
assert!(vk.verify_internal(&[M], &sig));
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps consider proptest for these sorts of tests.

You can look at https://github.com/RustCrypto/crypto-bigint/tree/master/tests for some examples.

(Or I can try to convert them to proptests as a followup)

Copy link
Member

Choose a reason for hiding this comment

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

Note: tried doing this myself and it's a tad tricky due to the use of generics

Copy link
Member

Choose a reason for hiding this comment

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

Okay, found an okay enough solution and opened a PR to add proptests: #896

It also required adding a Debug impl for Signature as this PR does also.

It was able to find some failures right away, though I did see many_round_trip_test find failures with this PR's current state that the proptests did not (but perhaps they just need to be run more).

@json420
Copy link
Contributor

json420 commented Feb 12, 2025

Sorry, didn't realize this was ready to test already.

Still having problems, but with a new error:

thread 'ownedchain::tests::test_ownedchainstore' panicked at /home/jderose/.cargo/git/checkouts/signatures-7e4d4cb41d25b54f/92d2a82/ml-dsa/src/hint.rs:30:9:
internal error: entered unreachable code

@bifurcation
Copy link
Contributor Author

Yep, that looks like the same error I'm seeing locally. Still working on that one.

@bifurcation
Copy link
Contributor Author

@json420 Oddly, when I went to fix that remaining unreachable!() bug this morning, I was unable to reproduce it. It failed the first few times I tried the test, but then never after, despite hundreds of thousands of trials. Are you still seeing it?

@json420
Copy link
Contributor

json420 commented Feb 12, 2025

@bifurcation Huh, I hit what I pasted right away when I first ran my tests against your branch... but now I'm not getting them anymore. I'll test further.

@json420
Copy link
Contributor

json420 commented Feb 12, 2025

Ok, I can still hit the error (sometimes) in my tests:

thread 'ownedchain::tests::test_ocs_secret_to_public' panicked at /home/jderose/.cargo/git/checkouts/signatures-7e4d4cb41d25b54f/f1e4e30/ml-dsa/src/hint.rs:30:9:
internal error: entered unreachable code

I'll work on narrowing it down to known inputs.

@json420
Copy link
Contributor

json420 commented Feb 12, 2025

@bifurcation I still don't have reproduce-able values, but I have a theory: "internal error: entered unreachable code" is only being hit on legitimately invalid signature values. I've ran through millions of valid signatures and haven't hit this, but one difference in my zebrachain tests is that I'm testing bit-flip permutations, which in some cases should produce a signature that shouldn't decode (right?).

@tarcieri
Copy link
Member

Perhaps you can change the unreachable! to print out the signature when it’s hit?

@bifurcation
Copy link
Contributor Author

@json420 - Bit flips could certainly cause the decoding to fail, but that unreachable!() should actually be unreachable. So I would appreciate it if you could send a signature that's tripping it and I can dig into it.

@tarcieri - Should we consider this unreachable!() issue an independent problem, and go ahead and merge this fix? I am also reading up on proptest, per your suggestion above, but would propose handling that as a follow-on.

@bifurcation bifurcation marked this pull request as ready for review February 12, 2025 17:07
@bifurcation
Copy link
Contributor Author

Actually @tarcieri please hold, it started failing for me again.

@bifurcation
Copy link
Contributor Author

This test case reliably fails:

    #[test]
    fn bad_seed() {
        const BAD_SEED: &[u8] = &[
            223, 116, 86, 28, 89, 196, 31, 159, 108, 178, 58, 1, 14, 198, 242, 131, 243, 77, 188,
            21, 25, 108, 151, 27, 232, 208, 147, 229, 245, 113, 178, 114,
        ];

        let seed = B32::try_from(BAD_SEED).unwrap();

        let kp = MlDsa87::key_gen_internal(&seed);
        let sk = kp.signing_key;
        let vk = kp.verifying_key;

        let M = b"Hello world";
        let rnd = Array([0u8; 32]);
        let sig = sk.sign_internal(&[M], &rnd);

        let sig_enc = sig.encode();
        let sig_dec = Signature::<MlDsa87>::decode(&sig_enc).unwrap();
        let _ver = vk.verify_internal(&[M], &sig); // <-- this will panic
    }

@json420
Copy link
Contributor

json420 commented Feb 12, 2025

@bifurcation yup, that one fails for me too. Also fails against git master..

@bifurcation
Copy link
Contributor Author

OK, I believe the latest commit fixes this secondary issue. As with the first issue, we were being too strict in our assumptions about what ranges of numbers would appear in a given setting. The r0 value that caused the panic was assumed to be in the range (-gamma2, gamma2] produced by mod_plus_minus(). However, r0 is actually produced by decompose(), which sometimes subtracts one from the r0 output. So the actual range of possible r0 outputs from decompose() is [-gamma2, gamma2] (including -gamma2 on the low end).

Once again, the actual fix is one character, changing > to >=. In the process of debugging, I added tests verifying the correctness of mod_plus_minus() and decompose(), which I have left in for posterity. These tests are good candidates for proptests, cf. #896.

@json420 - Could you confirm that this fix looks good to you?

@json420
Copy link
Contributor

json420 commented Feb 12, 2025

@bifurcation I haven't been able to get any failures since your last change. Thanks!

@bifurcation
Copy link
Contributor Author

OK @tarcieri I think this is ready to merge.

@tarcieri tarcieri merged commit b01c3b7 into RustCrypto:master Feb 12, 2025
4 checks passed
@tarcieri tarcieri mentioned this pull request Feb 12, 2025
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.

ml-dsa: some valid signature values don't successfully round trip through encode + decode
3 participants