Skip to content

fix: constant-time Ed sig order check#437

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

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

@MarkAtwood MarkAtwood commented Jul 9, 2026

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).

[fenrir-sweep:held]

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 */
}

@aidangarske aidangarske left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🐺 Skoll Code Review

Overall recommendation: COMMENT
Findings: 1 total — 1 posted, 0 skipped

Posted findings

  • [Medium] Boundary coverage missing for rewritten Ed signature order comparesrc/wp_ecx_sig.c:460-467, src/wp_ecx_sig.c:677-684

Review generated by Skoll.

Comment thread src/wp_ecx_sig.c
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--) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 [Medium] Boundary coverage missing for rewritten Ed signature order compare
💡 SUGGEST test

The PR replaces the scalar/order validation in both Ed25519 and Ed448 verification with new branchless comparison logic, but the existing ECX tests only cover valid signatures and generic bad signatures such as flipping sig[1], which mutates the R half rather than exercising S/order boundaries. This means endian mistakes, inverted mask operands, or equality handling regressions in the new compare could pass the current test suite.

Recommendation: Add focused regression coverage for the rewritten comparison, ideally table-driven cases for both Ed25519 and Ed448 with S values equal to the order, one below, one above, and differences in both high and low bytes. If the compare remains inline, consider factoring the magnitude check into a small static helper so the boundary cases can be unit-tested directly.

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.

5 participants