Skip to content

Conversation

@erickcestari
Copy link
Contributor

@erickcestari erickcestari commented Aug 21, 2025

Explicitly document that signature validation requirements differ based on the presence of the n field:

  • With n field: signature MUST be normalized lower-S form
  • Without n field: pubkey recovery accepts both high-S and low-S

This clarifies existing implementation behavior where secp256k1_ecdsa_verify (used with n field) requires normalized signatures, while secp256k1_ecdsa_recover (used without n field) accepts both normalized
and non-normalized signatures.

cc @real-or-random

@real-or-random
Copy link
Contributor

@t-bast
Copy link
Collaborator

t-bast commented Sep 8, 2025

Explicitly document that signature validation requirements differ based on the presence of the n field:

Why don't we just require lower-S form in all cases? That would be simpler, wouldn't it?

@erickcestari
Copy link
Contributor Author

Explicitly document that signature validation requirements differ based on the presence of the n field:

Why don't we just require lower-S form in all cases? That would be simpler, wouldn't it?

It’s an option, but in practice it would add complexity because of how secp256k1_ecdsa_recover works. Most implementations, when recovering the pubkey, pass the signature directly to secp256k1_ecdsa_recover without also calling secp256k1_ecdsa_verify, since verification isn’t required (secp256k1_ecdsa_recover guarantees a correct signature). The current approach matches what the majority of implementations already do, so only a few would need changes. If we mandated low-S everywhere, implementations would have to add extra checks in the recovery path to validate the S value before or after pubkey recovery, which makes things more complicated/redundant.

lnd:

	// If the destination pubkey was provided as a tagged field, use that
	// to verify the signature, if not do public key recovery.
	if decodedInvoice.Destination != nil {
		signature, err := sig.ToSignature()
		if err != nil {
			return nil, fmt.Errorf("unable to deserialize "+
				"signature: %v", err)
		}
		if !signature.Verify(hash, decodedInvoice.Destination) {
			return nil, fmt.Errorf("invalid invoice signature")
		}
	} else {
		headerByte := recoveryID + 27 + 4
		compactSign := append([]byte{headerByte}, sig.RawBytes()...)
		pubkey, _, err := ecdsa.RecoverCompact(compactSign, hash)
		if err != nil {
			return nil, err
		}
		decodedInvoice.Destination = pubkey
	}

clightining:

	if (!have_n) {
		struct pubkey k;
		if (!secp256k1_ecdsa_recover(secp256k1_ctx,
					     &k.pubkey,
					     &sig,
					     (const u8 *)&hash))
			return decode_fail(b11, fail,
					   "signature recovery failed");
		node_id_from_pubkey(&b11->receiver_id, &k);
	} else {
		struct pubkey k;
		/* n parsing checked this! */
		if (!pubkey_from_node_id(&k, &b11->receiver_id))
			abort();
		if (!secp256k1_ecdsa_verify(secp256k1_ctx, &b11->sig,
					    (const u8 *)&hash,
					    &k.pubkey))
			return decode_fail(b11, fail, "invalid signature");
	}

@t-bast
Copy link
Collaborator

t-bast commented Sep 8, 2025

Fair enough!

description.
- if a valid `n` field is provided:
- MUST use the `n` field to validate the signature instead of performing signature recovery.
- the signature MUST be normalized lower-S form.
Copy link
Collaborator

@Roasbeef Roasbeef Sep 8, 2025

Choose a reason for hiding this comment

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

Makes sense.

Before merge, we should also go through the test vectors to see if any of them are high-s, regenerating if so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are no BOLT-11 test vectors that cover signature validation using the n field. We should probably add some.

@real-or-random
Copy link
Contributor

implementations would have to add extra checks in the recovery path to validate the S value before or after pubkey recovery, which makes things more complicated/redundant.

We could totally provide a function in libsecp256k1 to make that easy, but I still think that not insisting on low-s is the simplest thing to do here.

Some nits:

  • The operation is called "pubkey recovery", not "signature recovery".
  • The entire BOLT doesn't even mention the word ECDSA. Maybe this should be added for clarity.

@erickcestari erickcestari force-pushed the clarify-bolt11-low-s-signature branch 2 times, most recently from 90d8764 to 2e10b7f Compare September 9, 2025 12:42
Copy link
Collaborator

@t-bast t-bast left a comment

Choose a reason for hiding this comment

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

Thanks for the clarification!

@erickcestari
Copy link
Contributor Author

I’ll also add a new test vector to cover public key recovery from a high-S signature.

I noticed that rust-lightning currently enforces low-S even when recovering a public key from a signature similar to lightning-kmp.

Explicitly document that signature validation requirements differ
based on the presence of the `n` field:

- With `n` field: signature MUST be normalized low-S
- Without `n` field: public-key recovery accepts both high-S and low-S

This clarifies existing implementation behavior where secp256k1_ecdsa_verify
(used with `n` field) requires normalized signatures, while
secp256k1_ecdsa_recover (used without `n` field) accepts both normalized
and non-normalized signatures.
@erickcestari erickcestari force-pushed the clarify-bolt11-low-s-signature branch from aa8b063 to aa85c9d Compare September 9, 2025 17:27
t-bast added a commit to ACINQ/eclair that referenced this pull request Sep 10, 2025
We must accept both high-S and low-S signatures in Bolt 11 invoices
when performing public key recovery (which matches secp256k1's
behavior).

See lightning/bolts#1284
t-bast added a commit to ACINQ/eclair that referenced this pull request Sep 10, 2025
We must accept both high-S and low-S signatures in Bolt 11 invoices
when performing public key recovery (which matches secp256k1's
behavior).

See lightning/bolts#1284
t-bast added a commit to ACINQ/lightning-kmp that referenced this pull request Sep 10, 2025
When the `nodeId` is provided, we must verify that the signature is
valid and is in normalized low-S form. When the `nodeId` is not
provided though, we perform signature recovery, which accepts both
low-S and high-S signatures and verifies its validity.

See lightning/bolts#1284
Copy link
Collaborator

@t-bast t-bast left a comment

Choose a reason for hiding this comment

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

ACK aa85c9d

t-bast added a commit to ACINQ/lightning-kmp that referenced this pull request Sep 10, 2025
When the `nodeId` is provided, we must verify that the signature is
valid and is in normalized low-S form. When the `nodeId` is not
provided though, we perform signature recovery, which accepts both
low-S and high-S signatures and verifies its validity.

See lightning/bolts#1284
docker-lordvf6ik added a commit to docker-lordvf6ik/lightning-kmp that referenced this pull request Sep 28, 2025
When the `nodeId` is provided, we must verify that the signature is
valid and is in normalized low-S form. When the `nodeId` is not
provided though, we perform signature recovery, which accepts both
low-S and high-S signatures and verifies its validity.

See lightning/bolts#1284
ebonyschneider462359 added a commit to ebonyschneider462359/lightning-kmp that referenced this pull request Oct 10, 2025
When the `nodeId` is provided, we must verify that the signature is
valid and is in normalized low-S form. When the `nodeId` is not
provided though, we perform signature recovery, which accepts both
low-S and high-S signatures and verifies its validity.

See lightning/bolts#1284
Copy link
Collaborator

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM 🍦

@rustyrussell
Copy link
Collaborator

Ack, thanks!

@rustyrussell rustyrussell merged commit 0cf2151 into lightning:master Nov 17, 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.

5 participants