common: follow BOLT 4 requirements to make decryption constant time.
What changed, and why it matters
This change fixes a privacy weakness in how Core Lightning processes returned error messages from the Lightning network routing protocol. Previously, the software stopped decrypting as soon as it found the error's origin, which could let a malicious routing node figure out its position in the payment path by measuring how quickly the sender reacted. The patch now always performs 27 decryption rounds and uses dummy keys after the real origin is found, following the BOLT 4 specification's 'paranoid' advice to make the process take the same amount of time regardless of where the failure occurred.
Apply the patch. No immediate incident response is required, but operators running nodes that process multi-hop payments should upgrade to prevent route-position leakage via timing analysis. Review related Sphinx/onion handling code for other early-exit timing discrepancies.
Security signals we found
Timing side-channel mitigation in cryptographic error handling
Constant-time / fixed-iteration decryption loop
Dummy secret substitution for out-of-range hops
Removal of early-break on HMAC validation success
Explicit reference to BOLT 4 paranoid requirements
Privacy protection for payment sender route position
Evidence from the diff
The patch modifies unwrap_onionreply() in common/sphinx.c to implement constant-time error decryption per BOLT #4. The original loop iterated only over numhops shared secrets and broke on the first valid HMAC, leaking the origin index through timing. The new loop always runs 27 iterations (the maximum route length for TLV payload type). For indices below numhops it uses the real shared_secrets[i]; for indices beyond the actual route it uses a fixed dummy secret (memset to 0x07). It no longer breaks early on HMAC match, instead records the origin_index and captured payload but continues decrypting. The return value is set only if a valid HMAC was found, otherwise remains NULL. This removes a timing side channel that could reveal route position to an adversary performing repeated timing analysis.
Changed components
common/sphinx.cunwrap_onionreply()Lightning onion routing error reply handlingInspect captured patch +30 / −13
diff --git a/common/sphinx.c b/common/sphinx.c
index b8497b6..6761c79 100644
--- a/common/sphinx.c
+++ b/common/sphinx.c
@@ -834,22 +834,43 @@ u8 *unwrap_onionreply(const tal_t *ctx,
struct onionreply *r;
const u8 *cursor;
size_t max;
- u16 msglen;
+ u8 *ret;
r = new_onionreply(tmpctx, reply->contents);
*origin_index = -1;
+ ret = NULL;
- for (int i = 0; i < numhops; i++) {
- struct secret key;
+ /* BOLT #4:
+ * The _origin node_:
+ * - once the return message has been decrypted:
+ * - SHOULD store a copy of the message.
+ * - SHOULD continue decrypting, until the loop has been repeated 27 times
+ * (maximum route length of tlv payload type).
+ * - SHOULD use constant `ammag` and `um` keys to obfuscate the route length.
+ *...
+ * ### Rationale
+ *
+ * The requirements for the _origin node_ should help hide the payment sender.
+ * By continuing decrypting 27 times (dummy decryption cycles after the error is found)
+ * the erroring node cannot learn its relative position in the route by performing
+ * a timing analysis if the sender were to retry the same route multiple times.
+ */
+ for (size_t i = 0; i < 27; i++) {
+ struct secret ss, key;
struct hmac hmac, expected_hmac;
+ if (i < numhops)
+ ss = shared_secrets[i];
+ else
+ memset(&ss, 0x7, sizeof(ss));
+
/* Since the encryption is just XORing with the cipher
* stream encryption is identical to decryption */
- r = wrap_onionreply(tmpctx, &shared_secrets[i], r);
+ r = wrap_onionreply(tmpctx, &ss, r);
/* Check if the HMAC matches, this means that this is
* the origin */
- subkey_from_hmac("um", &shared_secrets[i], &key);
+ subkey_from_hmac("um", &ss, &key);
cursor = r->contents;
max = tal_count(r->contents);
@@ -861,18 +882,14 @@ u8 *unwrap_onionreply(const tal_t *ctx,
compute_hmac(&key, cursor, max, NULL, 0, &expected_hmac);
if (hmac_eq(&hmac, &expected_hmac)) {
+ u16 msglen;
+ msglen = fromwire_u16(&cursor, &max);
+ ret = fromwire_tal_arrn(ctx, &cursor, &max, msglen);
*origin_index = i;
- break;
}
}
- /* Didn't find source, it's garbled */
- if (*origin_index == -1) {
- return NULL;
- }
-
- msglen = fromwire_u16(&cursor, &max);
- return fromwire_tal_arrn(ctx, &cursor, &max, msglen);
+ return ret;
}
struct onionpacket *sphinx_decompress(const tal_t *ctx,
Why this scored 67/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.