| Crates.io | nostringer |
| lib.rs | nostringer |
| version | 0.1.8 |
| created_at | 2025-03-28 09:59:03.417036+00 |
| updated_at | 2025-04-01 11:19:16.975818+00 |
| description | Ring signatures (SAG, BLSAG) for Nostr |
| homepage | https://github.com/AbdelStark/nostringer-rs |
| repository | https://github.com/AbdelStark/nostringer-rs |
| max_upload_size | |
| id | 1609477 |
| size | 245,733 |
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.
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.
Check ROADMAP.md for the detailed project roadmap, including completed and upcoming milestones.
sign and verify functions with compact format signatures for easier use.k256, sha2).The library offers two main variants of ring signatures:
The default variant that provides:
A linkable variant that:
Choose the variant that best suits your privacy and security requirements.
This library implements both a basic SAG-like ring signature and the bLSAG (Back's Linkable Spontaneous Anonymous Group) variant. They offer different properties with corresponding performance characteristics:
Functionality:
sign, verify, sign_binary, verify_binary):
sign_blsag_binary, verify_blsag_binary):
I) which is unique and deterministic for each private key (I = sk * H_p(PK)). If the same private key is used to create multiple bLSAG signatures (even with different rings or messages), they will all produce the same key image. This allows detection of multiple signatures from the same (anonymous) source, useful for preventing double-voting or double-spending in anonymous contexts. Signatures from different private keys will produce different key images.Signature Size:
c0, s): Contains n + 1 scalars (where n is the ring size).
32 * (n + 1) bytes.c0, s) + Key Image (I): Contains n + 1 scalars plus one key image (a curve point).
[32 * (n + 1)] bytes (signature) + 33 bytes (compressed key image) = 32n + 65 bytes.c0 and s values, making them slightly larger (a constant overhead of 33 bytes compared to SAG when using compressed points).Performance (Signing & Verification Speed):
The computational cost is dominated by elliptic curve scalar multiplications and hashing operations.
2n point multiplications per sign/verify operation in the main loop (s*G + c*P).4n point multiplications per sign/verify operation in the main loop (s*G + c*P and s*Hp(P) + c*I). It also includes the key image calculation (sk * Hp(PK)) during signing and a key image validity check (subgroup check via is_torsion_free) during verification.hash_to_scalar) involving the message, ring keys (hex strings in current implementation), and one point. This hash is computed n times per operation.
hash_to_point operation (hashing a public key to a point) for each ring member (n times per operation). It uses a different challenge hash function (hash_for_blsag_challenge) involving the message and two points, also computed n times per operation.hash_to_point). Therefore, bLSAG operations are expected to be noticeably slower than their SAG counterparts. We will provide detailed benchmarks to quantify this difference.Summary Table:
| Feature | SAG | bLSAG | Trade-off Summary |
|---|---|---|---|
| Linkability | No (Unlinkable) | Yes (Via Key Image) | bLSAG adds same-signer detection. |
| Size | 32(n+1) bytes |
32n + 65 bytes |
bLSAG is slightly larger (+33 bytes). |
| Speed | Faster (~2n mults) |
Slower (~4n mults + extras) |
bLSAG is computationally heavier. |
When to Choose:
Add this crate to your Cargo.toml dependencies:
[dependencies]
nostringer = "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.)
use nostringer::{sign, verify, SignatureVariant, generate_keypair_hex, Error};
fn main() -> Result<(), Error> {
// 1. Setup: Generate keys for the ring members
// Keys can be x-only, compressed, or uncompressed hex strings
let keypair1 = generate_keypair_hex("xonly");
let keypair2 = generate_keypair_hex("compressed");
let keypair3 = generate_keypair_hex("xonly");
let ring_pubkeys_hex: Vec<String> = vec![
keypair1.public_key_hex.clone(),
keypair2.public_key_hex.clone(), // Signer's key must be included
keypair3.public_key_hex.clone(),
];
// 2. Define the message to be signed (as bytes)
let message = b"This is a secret message to the group.";
// 3. Signer (keypair2) signs the message using their private key
println!("Signing message...");
// Use the top-level API with compact signature format
// Choose the signature variant: SignatureVariant::Sag (unlinkable) or SignatureVariant::Blsag (linkable)
let signature = sign(
message,
&keypair2.private_key_hex, // Signer's private key hex
&ring_pubkeys_hex, // The full ring of public keys
SignatureVariant::Sag // Use SAG variant (unlinkable)
)?;
println!("Generated Compact Signature: {}", signature);
// Output is a compact "ringA..." format string
// 4. Verification: Anyone can verify the signature against the ring and message
println!("\nVerifying signature...");
let is_valid = verify(
&signature,
message,
&ring_pubkeys_hex, // Must use the exact same ring (order matters for hashing)
)?;
println!("Signature valid: {}", is_valid);
assert!(is_valid);
// 5. Tamper test: Verification should fail if the message changes
println!("\nVerifying with tampered message...");
let tampered_message = b"This is a different message.";
let is_tampered_valid = verify(
&signature,
tampered_message,
&ring_pubkeys_hex,
)?;
println!("Tampered signature valid: {}", is_tampered_valid);
assert!(!is_tampered_valid);
Ok(())
}
The library provides two signature variants that you can select using the SignatureVariant enum:
use nostringer::{sign, verify, SignatureVariant, generate_keypair_hex, Error};
fn main() -> Result<(), Error> {
// Setup: Generate keys for the ring
let keypair1 = generate_keypair_hex("xonly");
let keypair2 = generate_keypair_hex("xonly");
let ring = vec![keypair1.public_key_hex.clone(), keypair2.public_key_hex.clone()];
let message = b"This is a message for the ring.";
// SAG variant (unlinkable - default)
// No way to tell if two signatures came from the same signer
let sag_signature = sign(
message,
&keypair1.private_key_hex,
&ring,
SignatureVariant::Sag // Use the SAG variant
)?;
// BLSAG variant (linkable)
// Same key produces the same key image, allowing detection of repeat signers
let blsag_signature = sign(
message,
&keypair1.private_key_hex,
&ring,
SignatureVariant::Blsag // Use the BLSAG variant
)?;
// Verify both types of signatures using the same verify function
// The signature format automatically determines which verification algorithm to use
assert!(verify(&sag_signature, message, &ring)?);
assert!(verify(&blsag_signature, message, &ring)?);
Ok(())
}
For applications requiring maximum performance, we also provide lower-level binary APIs that work directly with the native types, avoiding hex conversion overhead:
use nostringer::{sag, blsag, types::Error};
use k256::{Scalar, ProjectivePoint};
fn main() -> Result<(), Error> {
// Assuming you have raw binary keys available:
// (You'd normally get these from elsewhere in your app)
let private_key = /* Scalar value */;
let ring_pubkeys = /* Vec<ProjectivePoint> */;
let message = b"This is a secret message to the group.";
// Sign using binary SAG API (more efficient)
let binary_signature = sag::sign_binary(message, &private_key, &ring_pubkeys, rand::rngs::OsRng)?;
// Verify using binary SAG API (more efficient)
let is_valid = sag::verify_binary(&binary_signature, message, &ring_pubkeys)?;
println!("Signature valid: {}", is_valid);
Ok(())
}
Nostringer can be compiled to WebAssembly, allowing you to use it directly in web browsers and other WASM environments:
// Import the WASM module
import init, {
wasm_generate_keypair,
wasm_sign,
wasm_verify,
wasm_sign_blsag,
wasm_verify_blsag,
wasm_key_images_match,
} from "./nostringer.js";
// Initialize the WASM module
async function main() {
await init();
// Generate keypairs for the ring
const keypair1 = wasm_generate_keypair("xonly");
const keypair2 = wasm_generate_keypair("xonly");
const keypair3 = wasm_generate_keypair("xonly");
const ringPubkeys = [
keypair1.public_key_hex(),
keypair2.public_key_hex(),
keypair3.public_key_hex(),
];
// Sign a message with one of the keys
const message = new TextEncoder().encode(
"This is a secret message to the group.",
);
const signature = wasm_sign(message, keypair2.private_key_hex(), ringPubkeys);
// Verify the signature
const isValid = wasm_verify(signature, message, ringPubkeys);
console.log("Signature valid:", isValid);
}
main();
To compile Nostringer for WebAssembly:
# Install wasm-pack if you don't have it
cargo install wasm-pack
# Build the WASM module
wasm-pack build --target web --features wasm
# For bundlers like webpack
wasm-pack build --target bundler --features wasm
# For Node.js
wasm-pack build --target nodejs --features wasm
See the WebAssembly example for a complete demonstration of using Nostringer in a web browser.
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.
cargo run --example basic_signing
Key Formats (examples/key_formats.rs): Shows how to work with different key formats (x-only, compressed, uncompressed) and create larger rings.
cargo run --example key_formats
BLSAG Linkability (examples/blsag_linkability.rs): Demonstrates the linkable BLSAG variant and how to detect when the same key is used for multiple signatures.
cargo run --example blsag_linkability
Error Handling (examples/error_handling.rs): Demonstrates proper error handling for common error scenarios.
cargo run --example error_handling
WebAssembly (examples/web/basic_wasm): A web-based example showing how to use the library in a browser via WebAssembly.
# Build the WASM module
wasm-pack build crates/nostringer --target web --out-dir examples/web/basic_wasm/pkg --features wasm
# Serve the example (using Python's built-in server)
cd crates/nostringer/examples/web/basic_wasm
python -m http.server
These examples provide practical demonstrations of how to use the library in real-world scenarios and handle various edge cases.
The library includes comprehensive benchmarks using the Criterion framework for different ring sizes and operations. You can run these benchmarks yourself with:
cargo bench
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.
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:
MacBookPro18,2)arm6423H124)Check the Rust API Docs for detailed API reference and usage examples.
The size of the generated ring signature depends directly on the number of members (n) in the ring. It consists of:
c0) scalar (32 bytes binary / 64 hex chars).n response scalars (s array) (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.
n) and plausibility of the chosen ring members. Ensure the ring contains keys that could realistically be the signer in the given context.k256 crate. While k256 is well-regarded, this specific ring signature implementation has not been independently audited.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.
This project is licensed under the MIT License.
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:
