Skip to content

fix: constant-time Ed sig order check#437

Open
MarkAtwood wants to merge 1 commit into
wolfSSL:masterfrom
MarkAtwood:fix/ed-sig-order-check-const-time
Open

fix: constant-time Ed sig order check#437
MarkAtwood wants to merge 1 commit into
wolfSSL:masterfrom
MarkAtwood:fix/ed-sig-order-check-const-time

Conversation

@MarkAtwood

Copy link
Copy Markdown

Bug: wp_ed25519_digest_verify and wp_ed448_digest_verify (src/wp_ecx_sig.c) validate that the signature scalar s is below the curve order with a byte-by-byte compare that breaks on the first differing byte. Iteration count — and therefore execution time — depends on the position of the highest differing s byte, leaking information about s in a variable-time path.

Fix: replace the branchy loop in both functions with a branchless full-width magnitude compare built on the in-tree wp_ct_int_mask_lt helper (include/wolfprovider/internal.h, src/wp_internal.c). The scan runs a fixed number of iterations (32 for Ed25519, 57 for Ed448) with no early exit. Exact semantics preserved: reject iff s > curve_order (accept when equal or less).

Impact: Ed signatures are normally public, so this matters chiefly for blind / threshold signing constructions where s (or a component) is secret. Severity low.

Build-verified: compiled the translation unit with gcc -c -O2 -Wall against a wolfSSL --enable-all install + system OpenSSL headers (EXIT_CC=0). Disassembly of both functions confirms the loop terminator is now a fixed pointer-vs-end compare (data-independent iteration count) and the prior data-dependent early-break jne is gone; the two wp_ct_int_mask_lt calls are branchless.

Reported by static analysis (Fenrir finding 841).

Replace the byte-by-byte s < curve-order comparison in
wp_ed25519_digest_verify and wp_ed448_digest_verify with a
branchless full-width magnitude compare using the existing
wp_ct_int_mask_lt helper. The old loop broke early on the first
differing byte, making iteration count (and thus timing) depend on
the position of the highest differing s byte. Semantics preserved:
reject iff s > curve order.
Copilot AI review requested due to automatic review settings July 9, 2026 23:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes a variable-time signature scalar (s) vs curve-order check in Ed25519/Ed448 verification by replacing an early-exit byte-compare loop with a constant-time full-width magnitude compare.

Changes:

  • Replaced the branchy (early-break) order check in wp_ed25519_digest_verify with a fixed-iteration constant-time compare using wp_ct_int_mask_lt.
  • Applied the same constant-time compare pattern to wp_ed448_digest_verify.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/wp_ecx_sig.c
Comment on lines +456 to +467
byte gt = 0; /* s > order found at a more-significant byte */
byte lt = 0; /* s < order found at a more-significant byte */
/* Constant-time full-width magnitude compare, big-endian scan with no
* early break: reject iff s > order (accept when equal or less). */
for (i = ED25519_KEY_SIZE - 1; i >= 0; i--) {
if (sig[ED25519_KEY_SIZE + i] > wp_ed25519_order[i]) {
ok = 0;
}
if (sig[ED25519_KEY_SIZE + i] != wp_ed25519_order[i]) {
break;
}
byte s = sig[ED25519_KEY_SIZE + i];
byte o = wp_ed25519_order[i];
byte eq = (byte)~(gt | lt); /* still tied above */
gt |= (byte)(eq & wp_ct_int_mask_lt(o, s)); /* o < s => s > order */
lt |= (byte)(eq & wp_ct_int_mask_lt(s, o)); /* s < order */
}
ok &= (int)(1u & ~(unsigned int)gt);
Comment thread src/wp_ecx_sig.c
Comment on lines +673 to +684
byte gt = 0; /* s > order found at a more-significant byte */
byte lt = 0; /* s < order found at a more-significant byte */
/* Constant-time full-width magnitude compare, big-endian scan with no
* early break: reject iff s > order (accept when equal or less). */
for (i = ED448_KEY_SIZE - 1; i >= 0; i--) {
if (sig[ED448_KEY_SIZE + i] > wp_ed448_order[i]) {
ok = 0;
}
if (sig[ED448_KEY_SIZE + i] != wp_ed448_order[i]) {
break;
}
byte s = sig[ED448_KEY_SIZE + i];
byte o = wp_ed448_order[i];
byte eq = (byte)~(gt | lt); /* still tied above */
gt |= (byte)(eq & wp_ct_int_mask_lt(o, s)); /* o < s => s > order */
lt |= (byte)(eq & wp_ct_int_mask_lt(s, o)); /* s < order */
}
ok &= (int)(1u & ~(unsigned int)gt);
Comment thread src/wp_ecx_sig.c
Comment on lines +458 to 466
/* Constant-time full-width magnitude compare, big-endian scan with no
* early break: reject iff s > order (accept when equal or less). */
for (i = ED25519_KEY_SIZE - 1; i >= 0; i--) {
if (sig[ED25519_KEY_SIZE + i] > wp_ed25519_order[i]) {
ok = 0;
}
if (sig[ED25519_KEY_SIZE + i] != wp_ed25519_order[i]) {
break;
}
byte s = sig[ED25519_KEY_SIZE + i];
byte o = wp_ed25519_order[i];
byte eq = (byte)~(gt | lt); /* still tied above */
gt |= (byte)(eq & wp_ct_int_mask_lt(o, s)); /* o < s => s > order */
lt |= (byte)(eq & wp_ct_int_mask_lt(s, o)); /* s < order */
}
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.

3 participants