Blinding factor handling #321

Closed
opened 2025-12-12 11:05:35 +00:00 by robwoodgate · 5 comments
robwoodgate commented 2025-12-12 11:05:35 +00:00 (Migrated from github.com)

Our core protocol in NUT 00 says:

Alice sends to Bob: B_ = Y + rG with r being a random blinding factor

However, it doesn’t explicitly state that r is a secp256k1 scalar in Z_n* ( i.e: "a non zero element in the range 1..n−1, where n is the curve order"), or how to handle values outside this canonical range.

This seems to be assumed elsewhere. In NUT 13, for example, the blinded message section derives r via:

r = self.bip32.get_privkey_from_path(r_derivation_path)

and BIP32 derived private keys are commonly treated as scalars modulo n (with 0 treated as invalid and retried).

EDIT: Apparently not!

Why this matters

In practice this matters for interoperability because different implementations may:

  • accept any integer and implicitly reduce it modulo n,
  • reject scalars outside 1..n−1,
  • or behave inconsistently in the r = 0 edge case.

It also matters for privacy. If r ≡ 0 (mod n), then B_ = Y + rG collapses to B_ = Y, so blinding is lost for that output.

Questions:

  • Should the blinding factor r be defined as a scalar in Z_n*, meaning implementations MUST interpret it modulo n and canonicalise it to 1..n−1, rather than rejecting “out of range” inputs?
  • Should r ≡ 0 (mod n) be explicitly invalid, and if so, should NUT 13 specify a deterministic retry mechanism (for example, rejection sampling / re-derivation with an internal counter) to avoid restore failures and ensure cross implementation determinism?
Our [core protocol](https://github.com/cashubtc/nuts/blob/main/00.md#protocol) in NUT 00 says: `Alice sends to Bob: B_ = Y + rG with r being a random blinding factor` However, it doesn’t explicitly state that `r` is a secp256k1 scalar in `Z_n*` ( i.e: _"a non zero element in the range 1..n−1, where `n` is the curve order"_), or how to handle values outside this canonical range. This seems to be assumed elsewhere. In NUT 13, for example, the [blinded message section](https://github.com/cashubtc/nuts/blob/main/13.md#generate-blindedmessages) derives `r` via: ``` r = self.bip32.get_privkey_from_path(r_derivation_path) ``` ~~and BIP32 derived private keys are commonly treated as scalars modulo `n` (with `0` treated as invalid and retried).~~ EDIT: [Apparently not](https://github.com/cashubtc/nuts/issues/321#issuecomment-3646430767)! ### Why this matters In practice this matters for interoperability because different implementations may: - accept any integer and implicitly reduce it modulo `n`, - reject scalars outside `1..n−1`, - or behave inconsistently in the `r = 0` edge case. It also matters for privacy. If `r ≡ 0 (mod n)`, then `B_ = Y + rG` collapses to `B_ = Y`, so blinding is lost for that output. ### Questions: - Should the blinding factor `r` be defined as a scalar in `Z_n*`, meaning implementations MUST interpret it modulo `n` and canonicalise it to `1..n−1`, rather than rejecting “out of range” inputs? - Should `r ≡ 0 (mod n)` be explicitly invalid, and if so, should NUT 13 specify a deterministic retry mechanism (for example, rejection sampling / re-derivation with an internal counter) to avoid restore failures and ensure cross implementation determinism?
Egge21M commented 2025-12-12 12:04:59 +00:00 (Migrated from github.com)

I think being explicit does not hurt, although in EC cryptography values are implicitly assumed to be curve-save, no?

I think being explicit does not hurt, although in EC cryptography values are implicitly assumed to be curve-save, no?
robwoodgate commented 2025-12-12 12:25:09 +00:00 (Migrated from github.com)

I think being explicit does not hurt, although in EC cryptography values are implicitly assumed to be curve-save, no?

For general blinded messages, we can assume curve order for r is what we EXPECT, but handling of the edge cases is open to interpretatation.

The r = 0n case is problematic. In hash_to_curve, we explicitly retry until a valid point it found. No problem.

In NUT-13, however, we don't have a explicit canonical method to resolve, without knowing the internals of self.bip32.get_privkey_from_path.

> I think being explicit does not hurt, although in EC cryptography values are implicitly assumed to be curve-save, no? For general blinded messages, we can assume curve order for `r` is what we EXPECT, but handling of the edge cases is open to interpretatation. The `r = 0n` case is problematic. In `hash_to_curve`, we explicitly retry until a valid point it found. No problem. In NUT-13, however, we don't have a explicit canonical method to resolve, without knowing the internals of `self.bip32.get_privkey_from_path`.
Egge21M commented 2025-12-12 13:13:07 +00:00 (Migrated from github.com)

In NUT-13, however, we don't have a explicit canonical method to resolve, without knowing the internals of self.bip32.get_privkey_from_path.

IMO when NUTs references other specs, we should simply assume that implementations of those specifications implement correctly. In this case this is stated in BIP32:

In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.)

> In NUT-13, however, we don't have a explicit canonical method to resolve, without knowing the internals of `self.bip32.get_privkey_from_path`. IMO when NUTs references other specs, we should simply assume that implementations of those specifications implement correctly. In this case this is stated in BIP32: > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.)
robwoodgate commented 2025-12-12 14:25:35 +00:00 (Migrated from github.com)

IMO when NUTs references other specs, we should simply assume that implementations of those specifications implement correctly. In this case this is stated in BIP32:

In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.)

I tend to agree, however, the NUT references a specific Python implementation of the BIP32 spec.

And in the case of BIP32, it is not immediately obvious whether the stated issue is resolved within the spec, so that a dervation path always resolves to something canonically, or whether it returns nonsense expecting the caller to then do the counter increment and try again.

IMO, a little clarity hurts much less than misinterpretation.

> IMO when NUTs references other specs, we should simply assume that implementations of those specifications implement correctly. In this case this is stated in BIP32: > > > In case parse256(IL) ≥ n or ki = 0, the resulting key is invalid, and one should proceed with the next value for i. (Note: this has probability lower than 1 in 2127.) I tend to agree, however, the NUT references a specific Python implementation of the BIP32 spec. And in the case of BIP32, it is not immediately obvious whether the stated issue is resolved _within_ the spec, so that a dervation path always resolves to something canonically, or whether it returns nonsense expecting the caller to then do the counter increment and try again. IMO, a little clarity hurts much less than misinterpretation.
robwoodgate commented 2025-12-12 14:28:51 +00:00 (Migrated from github.com)

Have added a small PR with some suggestions for clarifications:
https://github.com/cashubtc/nuts/pull/322

Tl;DR: we assume scalars are modulo n unless othewise noted (eg NUT-13, NUT-26)

Have added a small PR with some suggestions for clarifications: https://github.com/cashubtc/nuts/pull/322 Tl;DR: we assume scalars are `modulo n` unless othewise noted (eg NUT-13, [NUT-26](https://github.com/cashubtc/nuts/pull/300))
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo-admin/nuts#321
No description provided.