Nostringer Ring Signatures (Rust)
A blazing fast Rust implementation of the Nostringer unlinkable ring signature scheme for Nostr, compatible with the nostringer TypeScript library.
Built using pure Rust crypto crates, this library allows a signer to prove membership in a group of Nostr accounts (defined by their public keys) without revealing which specific account produced the signature. It uses a Spontaneous Anonymous Group (SAG)-like algorithm compatible with secp256k1 keys used in Nostr.
Nostringer is largely inspired by Monero's Ring Signatures using Spontaneous Anonymous Group signatures (SAG), and beritani/ring-signatures implementation of ring signatures using the elliptic curve Ed25519 and Keccak for hashing.
Table of Contents
- Nostringer Ring Signatures (Rust)
- Table of Contents
- Disclaimer
- Problem Statement
- Key Features
- Installation
- Usage
- Examples
- Benchmarks
- API Reference
sign(message: &[u8], private_key_hex: &str, ring_pubkeys_hex: &[String]) -> Result<RingSignature, Error>verify(signature: &RingSignature, message: &[u8], ring_pubkeys_hex: &[String]) -> Result<bool, Error>sign_binary(message: &[u8], private_key: &Scalar, ring_pubkeys: &[ProjectivePoint]) -> Result<RingSignatureBinary, Error>verify_binary(signature: &RingSignatureBinary, message: &[u8], ring_pubkeys: &[ProjectivePoint]) -> Result<bool, Error>sign_with_hex(message: &[u8], private_key_hex: &str, ring_pubkeys_hex: &[String]) -> Result<RingSignature, Error>verify_with_hex(signature: &RingSignature, message: &[u8], ring_pubkeys_hex: &[String]) -> Result<bool, Error>generate_keypair_hex(format: &str) -> KeyPairHexRingSignatureStructRingSignatureBinaryStructKeyPairHexStructKeyPairStructErrorEnum
- Signature Size
- Security Considerations
- License
- References
Disclaimer
This code is highly experimental. The original author is not a cryptographer, and this Rust port, while aiming for compatibility and correctness using standard libraries, has not been audited or formally verified. Use for educational exploration at your own risk. Production usage is strongly discouraged until thorough security reviews and testing are performed by qualified individuals.
Problem Statement
In many scenarios, you want to prove that "someone among these N credentials produced this signature," but you do not want to reveal which credential or identity. For instance, you might have a set of recognized Nostr pubkeys (e.g., moderators, DAO members, authorized reviewers) who are allowed to perform certain actions, but you want them to remain anonymous within that set when doing so.
A ring signature solves this by letting an individual sign a message on behalf of the group (the ring). A verifier can confirm the message originated from one of the public keys in the ring, without learning the specific signer's identity.
Key Features
- Unlinkable: Signatures hide the signer's identity. Two signatures from the same signer cannot be linked cryptographically.
- Linkable Option: The BLSAG variant provides linkability through key images to detect when the same key is used multiple times, while still preserving anonymity within the ring.
- Fast: Implemented in Rust, leveraging efficient and audited cryptographic primitives from the RustCrypto ecosystem (
k256,sha2). - Optimized API: Provides both hex-string based API and a more efficient binary API that avoids serialization/deserialization overhead.
- Nostr Key Compatibility: Directly supports standard Nostr key formats (hex strings):
- 32-byte (64-hex) x-only public keys.
- 33-byte (66-hex) compressed public keys.
- 65-byte (130-hex) uncompressed public keys.
- 32-byte (64-hex) private keys.
- Easy to Use: Simple
sign,verify, andgenerate_keypair_hexfunctions. - Minimal Dependencies: Relies on well-maintained RustCrypto crates.
- No Trusted Setup: The scheme does not require any special setup ceremony.
Installation
Add this crate to your Cargo.toml dependencies:
[]
= "0.1.0" # Replace with the latest version from crates.io
(Note: You might need other crates like hex or rand in your own project depending on how you handle keys and messages.)
Usage
use ;
Optimized Binary API
For applications requiring maximum performance, we provide a binary API that works directly with the native types, avoiding hex conversion overhead:
use ;
use ;
Examples
The repository includes several examples that demonstrate different aspects of the library:
-
Basic Signing (
examples/basic_signing.rs): Demonstrates the core signing and verification functionality. -
Key Formats (
examples/key_formats.rs): Shows how to work with different key formats (x-only, compressed, uncompressed) and create larger rings. -
BLSAG Linkability (
examples/blsag_linkability.rs): Demonstrates the linkable BLSAG variant and how to detect when the same key is used for multiple signatures. -
Error Handling (
examples/error_handling.rs): Demonstrates proper error handling for common error scenarios.
These examples provide practical demonstrations of how to use the library in real-world scenarios and handle various edge cases.
Benchmarks
The library includes comprehensive benchmarks using the Criterion framework for different ring sizes and operations. You can run these benchmarks yourself with:
For detailed information on running and interpreting benchmarks, see BENCHMARKS.md.
The repository also includes a GitHub Actions workflow that automatically runs benchmarks on each push and pull request, with the HTML report available as an artifact in the workflow run.
Performance Results
Below is a summary of the benchmark results, showing median execution times for each operation with different ring sizes:
| Operation | Ring Size | Execution Time |
|---|---|---|
| Sign | 2 members | 204.75 µs |
| Sign | 10 members | 897.76 µs |
| Sign | 100 members | 13.31 ms |
| Verify | 2 members | 166.83 µs |
| Verify | 10 members | 847.23 µs |
| Verify | 100 members | 12.71 ms |
| Sign+Verify | 2 members | 370.41 µs |
| Sign+Verify | 10 members | 1.76 ms |
| Sign+Verify | 100 members | 25.02 ms |
Benchmarking Environment:
- Model: MacBook Pro (Identifier:
MacBookPro18,2) - CPU: Apple M1 Max
- Cores: 10
- RAM: 64 GB
- Architecture:
arm64 - Operating System: macOS 14.7 (Build
23H124)
API Reference
sign(message: &[u8], private_key_hex: &str, ring_pubkeys_hex: &[String]) -> Result<RingSignature, Error>
Signs a message using the SAG-like ring signature scheme. This function is a wrapper around the more efficient sign_binary that handles hex conversion.
message: The message bytes (&[u8]) to sign.private_key_hex: The signer's private key as a 64-character hex string.ring_pubkeys_hex: A slice of public key hex strings representing the ring members. The signer's corresponding public key (or the key corresponding to the negated private key) must be present in this ring. The order of keys matters for verification.- Returns: A
Resultcontaining theRingSignatureon success, or anErroron failure (e.g., signer not in ring, invalid keys, ring too small).
verify(signature: &RingSignature, message: &[u8], ring_pubkeys_hex: &[String]) -> Result<bool, Error>
Verifies a ring signature against a message and the ring of public keys. This function is a wrapper around the more efficient verify_binary that handles hex conversion.
signature: A reference to theRingSignatureobject ({ c0, s }).message: The original message bytes (&[u8]) that were allegedly signed.ring_pubkeys_hex: A slice of public key hex strings representing the ring. Must be identical (including order) to the ring used during signing.- Returns: A
Resultcontainingtrueif the signature is valid for the message and ring, orfalseif it's invalid. Returns anErrorif inputs are malformed (e.g., wrong signature length, invalid hex).
sign_binary(message: &[u8], private_key: &Scalar, ring_pubkeys: &[ProjectivePoint]) -> Result<RingSignatureBinary, Error>
Optimized version of sign that works directly with binary types, avoiding hex conversion overhead.
message: The message bytes (&[u8]) to sign.private_key: The signer's private key as ak256::Scalar.ring_pubkeys: A slice of public keys ask256::ProjectivePointrepresenting the ring members.- Returns: A
Resultcontaining theRingSignatureBinaryon success, or anErroron failure.
verify_binary(signature: &RingSignatureBinary, message: &[u8], ring_pubkeys: &[ProjectivePoint]) -> Result<bool, Error>
Optimized version of verify that works directly with binary types, avoiding hex conversion overhead.
signature: A reference to theRingSignatureBinaryobject.message: The original message bytes (&[u8]) that were allegedly signed.ring_pubkeys: A slice of public keys ask256::ProjectivePointrepresenting the ring.- Returns: A
Resultcontainingtrueif the signature is valid, orfalseif it's invalid.
sign_with_hex(message: &[u8], private_key_hex: &str, ring_pubkeys_hex: &[String]) -> Result<RingSignature, Error>
Alias for the original sign function, provided for clarity. Handles hex conversion internally.
verify_with_hex(signature: &RingSignature, message: &[u8], ring_pubkeys_hex: &[String]) -> Result<bool, Error>
Alias for the original verify function, provided for clarity. Handles hex conversion internally.
generate_keypair_hex(format: &str) -> KeyPairHex
Generates a new random secp256k1 key pair.
format: A string slice specifying the desired public key format:"xonly": 64-hex (32 bytes), guaranteed even-Y point."compressed": 66-hex (33 bytes), starts with02or03."uncompressed": 130-hex (65 bytes), starts with04.- Defaults to
"compressed"if an unrecognized format is provided.
- Returns: A
KeyPairHexstruct containingprivate_key_hex(String) andpublic_key_hex(String). Note: The returnedprivate_key_hexis the original randomly generated scalar, even if internal negation was required to produce an even-Y public key for the"xonly"format.
RingSignature Struct
RingSignatureBinary Struct
BlsagSignature Struct
BlsagSignatureBinary Struct
KeyImage Struct
;
A struct representing a key image for linkable signatures. Key images uniquely identify the signer's private key without revealing it. Provided with methods to convert to/from hex strings and compare for equality.
KeyPairHex Struct
KeyPair Struct
Error Enum
An enum representing possible errors during signing or verification, such as invalid key formats, signer not found in the ring, ring too small, hex decoding errors, or internal cryptographic errors.
sign_blsag_binary(message: &[u8], private_key: &Scalar, ring_pubkeys: &[ProjectivePoint]) -> Result<(BlsagSignatureBinary, KeyImage), Error>
Creates a BLSAG (linkable) signature using binary inputs.
message: The message bytes (&[u8]) to sign.private_key: The signer's private key as ak256::Scalar.ring_pubkeys: A slice of public keys ask256::ProjectivePointrepresenting the ring members.- Returns: A
Resultcontaining a tuple of(BlsagSignatureBinary, KeyImage)on success, or anErroron failure. The KeyImage can be used to detect when the same key is used for multiple signatures.
verify_blsag_binary(signature: &BlsagSignatureBinary, key_image: &KeyImage, message: &[u8], ring_pubkeys: &[ProjectivePoint]) -> Result<bool, Error>
Verifies a BLSAG (linkable) signature using binary inputs.
signature: The binary BLSAG signature to verify.key_image: The key image associated with the signature.message: The message bytes that were allegedly signed.ring_pubkeys: A slice of public keys ask256::ProjectivePointrepresenting the ring.- Returns: A
Resultcontainingtrueif the signature is valid, orfalseif it's invalid.
sign_blsag_hex(message: &[u8], private_key_hex: &str, ring_pubkeys_hex: &[String]) -> Result<(BlsagSignature, String), Error>
Creates a BLSAG (linkable) signature using hex inputs.
message: The message bytes (&[u8]) to sign.private_key_hex: The signer's private key as a 64-character hex string.ring_pubkeys_hex: A slice of public key hex strings representing the ring.- Returns: A
Resultcontaining a tuple of(BlsagSignature, String)on success, or anErroron failure. The second element is the key image as a hex string.
verify_blsag_hex(signature_hex: &BlsagSignature, key_image_hex: &str, message: &[u8], ring_pubkeys_hex: &[String]) -> Result<bool, Error>
Verifies a BLSAG (linkable) signature using hex inputs.
signature_hex: The hex BLSAG signature to verify.key_image_hex: The key image hex string associated with the signature.message: The message bytes that were allegedly signed.ring_pubkeys_hex: A slice of public key hex strings representing the ring.- Returns: A
Resultcontainingtrueif the signature is valid, orfalseif it's invalid.
key_images_match(image1: &KeyImage, image2: &KeyImage) -> bool
Compares two key images to determine if they were created by the same signer.
image1: The first key image to compare.image2: The second key image to compare.- Returns:
trueif the key images match (same signer),falseotherwise.
Signature Size
The size of the generated ring signature depends directly on the number of members (n) in the ring. It consists of:
- One initial challenge (
c0) scalar (32 bytes binary / 64 hex chars). nresponse scalars (sarray) (each 32 bytes binary / 64 hex chars).
The total binary size follows the formula:
Size (bytes) = 32 * (n + 1)
This means the signature size grows linearly with the ring size. A larger ring provides more anonymity but results in a larger signature.
Security Considerations
- Anonymity Set: The level of anonymity depends on the size (
n) and plausibility of the chosen ring members. Ensure the ring contains keys that could realistically be the signer in the given context. - No Trusted Setup: This scheme does not require any trusted setup procedure.
- Unlinkability vs. Linkability:
- SAG: The default SAG implementation provides complete unlinkability. Signatures produced by the same signer for different messages (using the same or different rings) are cryptographically unlinkable.
- BLSAG: The BLSAG variant intentionally provides linkability through key images. These key images allow detecting when the same key signed multiple messages, while still preserving anonymity (not revealing which specific ring member is the signer).
- Implementation Security: This library relies on the correctness of the underlying
k256crate. Whilek256is well-regarded, this specific ring signature implementation has not been independently audited.
Signature Variants
The library offers two main variants of ring signatures:
1. SAG (Spontaneous Anonymous Group)
The default variant that provides:
- Complete unlinkability (no way to tell if two signatures came from the same signer)
- Maximum privacy within the ring
- Suitable for anonymous voting, whistleblowing, or any scenario requiring maximum privacy
2. BLSAG (Back's Linkable Spontaneous Anonymous Group)
A linkable variant that:
- Produces a key image along with the signature to enable linkability
- Can detect when the same key signs multiple times (via the key image)
- Still doesn't reveal which specific ring member signed (preserves anonymity within the ring)
- Suitable for preventing double-spending, duplicate voting, or tracking usage of a credential
- Similar to the linkable ring signature scheme used in Monero
Choose the variant that best suits your privacy and security requirements.
License
This project is licensed under the MIT License.
References
- Linkable Spontaneous Anonymous Group Signature for Ad Hoc Groups - (Joseph Liu et al., 2004) – basis of LSAG.
- Beritani, ring-signatures JS library – Ed25519 ring signature implementation (SAG, bLSAG, MLSAG, CLSAG).
- Blockstream Elements rust-secp256k1-zkp library – Whitelist Ring Signature in libsecp256k1-zkp (C code exposed via Rust).
- Zero to Monero 2.0 – Chapter 3, ring signature algorithms.
- Cronokirby Blog – On Monero's Ring Signatures, explains Schnorr ring signatures in detail.
Built with love by AbdelStark 🧡
Feel free to follow me on Nostr if you'd like, using my public key:
npub1hr6v96g0phtxwys4x0tm3khawuuykz6s28uzwtj5j0zc7lunu99snw2e29
Or just scan this QR code to find me:
