fix(paynym): use Bitcoin message signature for PayNym claim
What changed, and why it matters
This commit changes how Stack Wallet proves ownership of a PayNym identity. PayNyms are reusable payment codes in Bitcoin-style wallets. The old code signed raw bytes directly with the notification key; the new code signs using the standard 'Bitcoin message' format (the same kind of signature used by wallet message-signing features). The change likely fixes a compatibility or security issue where PayNym servers or counterparties could not properly verify the old signature, which could prevent claiming a PayNym or allow signature misuse.
Review whether the old raw-byte signature could be verified or replayed in unintended contexts, and confirm that the new Bitcoin message signature is accepted by the PayNym service. Consider adding tests for PayNym claim signature verification and rotating or re-claiming PayNyms if the old signatures were accepted by any relying party.
Security signals we found
Changed cryptographic signature scheme for identity claim
Switched from raw ECDSA signature to Bitcoin message signature format
PayNym claim verification depends on this signature
No explicit CVE or advisory referenced in commit
Evidence from the diff
The patch replaces signStringWithNotificationKey’s implementation. Previously it called signWithNotificationKey on the UTF-8 bytes of the input string and returned the raw signature bytes as a string. Now it derives the notification BIP32 node, creates an ECPrivateKey, strips a leading length byte from the network message prefix if present, and uses coinlib.MessageSignature.sign with the cleaned prefix, returning a base64-encoded compact signature. This aligns the signature with Bitcoin message signing semantics, which is what PayNym claim/verification expects.
Changed components
lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dartPayNym claim/signStringWithNotificationKeyNotification key derivation and signingInspect captured patch +19 / −3
diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart
index 0d99303..bb69584 100644
--- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart
+++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart
@@ -342,10 +342,26 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface>
}
Future<String> signStringWithNotificationKey(String data) async {
- final bytes = await signWithNotificationKey(
- Uint8List.fromList(utf8.encode(data)),
+ final myPrivateKeyNode = await deriveNotificationBip32Node();
+ final key = coinlib.ECPrivateKey(myPrivateKeyNode.privateKey!);
+
+ // Clean prefix: strip leading length byte if present (coinlib recalculates)
+ final prefixBytes =
+ cryptoCurrency.networkParams.messagePrefix.toUint8ListFromUtf8;
+ final ignoreFirstByte =
+ prefixBytes.first == prefixBytes.length - 1;
+ final prefix = (ignoreFirstByte
+ ? prefixBytes.sublist(1)
+ : prefixBytes)
+ .toUtf8String;
+
+ final signed = coinlib.MessageSignature.sign(
+ key: key,
+ message: data,
+ prefix: prefix,
);
- return Format.uint8listToString(bytes);
+
+ return base64Encode(signed.signature.compact);
}
Future<TxData> preparePaymentCodeSend({
Why this scored 57/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.