<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nostringer WASM Demo</title>
<style>
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #4a4a4a;
text-align: center;
margin-bottom: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border-radius: 8px;
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
button {
background-color: #4caf50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
margin-bottom: 10px;
}
button:hover {
background-color: #45a049;
}
pre {
background-color: #f1f1f1;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
#tabs {
display: flex;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
background-color: #e0e0e0;
cursor: pointer;
border-radius: 4px 4px 0 0;
}
.tab.active {
background-color: #4caf50;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<h1>Nostringer Ring Signatures in WebAssembly</h1>
<div id="tabs">
<div class="tab active" data-tab="sag">SAG (Unlinkable)</div>
<div class="tab" data-tab="blsag">BLSAG (Linkable)</div>
<div class="tab" data-tab="compact">Compact Format</div>
</div>
<div class="container">
<div id="sag" class="tab-content active">
<div class="section">
<h2>SAG Ring Signatures</h2>
<p>
Standard ring signatures provide anonymity within a group without
linkability.
</p>
<button id="generate-keys">1. Generate Keys for Ring</button>
<button id="sign-message" disabled>2. Sign Message</button>
<button id="verify-signature" disabled>3. Verify Signature</button>
<button id="tamper-message" disabled>4. Tamper & Verify</button>
<pre id="sag-output">Click "Generate Keys for Ring" to start...</pre>
</div>
</div>
<div id="blsag" class="tab-content">
<div class="section">
<h2>BLSAG Linkable Ring Signatures</h2>
<p>
Linkable ring signatures allow detecting when the same key signs
multiple messages while maintaining anonymity.
</p>
<button id="blsag-generate-keys">1. Generate Keys for Ring</button>
<button id="blsag-sign-message1" disabled>
2. Sign First Message
</button>
<button id="blsag-sign-message2" disabled>
3. Sign Second Message
</button>
<button id="blsag-check-linkability" disabled>
4. Check Linkability
</button>
<pre id="blsag-output">
Click "Generate Keys for Ring" to start...</pre
>
</div>
</div>
<div id="compact" class="tab-content">
<div class="section">
<h2>Compact Signature Format</h2>
<p>
The compact format uses CBOR encoding and base64url for efficient
transmission of signatures. Compare both formats to see the size
difference.
</p>
<button id="compact-generate-keys">1. Generate Keys for Ring</button>
<button id="compact-sign-sag" disabled>
2. Sign with SAG (Unlinkable)
</button>
<button id="compact-sign-blsag" disabled>
3. Sign with BLSAG (Linkable)
</button>
<button id="compact-compare-sizes" disabled>
4. Compare Formats
</button>
<pre id="compact-output">
Click "Generate Keys for Ring" to start...</pre
>
</div>
</div>
</div>
<script type="module">
import init, {
wasm_generate_keypair,
wasm_sign,
wasm_verify,
wasm_sign_blsag,
wasm_verify_blsag,
wasm_key_images_match,
wasm_sign_compact_sag,
wasm_sign_compact_blsag,
wasm_verify_compact,
wasm_get_compact_signature_info,
} from "./pkg/nostringer.js";
async function initWasm() {
try {
await init();
console.log("WASM module initialized");
setupEventListeners();
} catch (error) {
console.error("Failed to initialize WASM module:", error);
document.getElementById("sag-output").textContent =
"Error initializing WASM module: " + error.message;
document.getElementById("blsag-output").textContent =
"Error initializing WASM module: " + error.message;
document.getElementById("compact-output").textContent =
"Error initializing WASM module: " + error.message;
}
}
const state = {
keypairs: [],
ringPubkeys: [],
signature: null,
message: "This is a secret message for the ring.",
};
const blsagState = {
keypairs: [],
ringPubkeys: [],
signatures: [],
keyImages: [],
messages: [
"First message: Approve the proposal.",
"Second message: Transfer the funds.",
],
};
const compactState = {
keypairs: [],
ringPubkeys: [],
message: "This message will be signed with different formats.",
signatures: {
sag: {
hex: null,
compact: null,
},
blsag: {
hex: null,
compact: null,
},
},
};
function setupEventListeners() {
document
.getElementById("generate-keys")
.addEventListener("click", generateKeys);
document
.getElementById("sign-message")
.addEventListener("click", signMessage);
document
.getElementById("verify-signature")
.addEventListener("click", verifySignature);
document
.getElementById("tamper-message")
.addEventListener("click", tamperAndVerify);
document
.getElementById("blsag-generate-keys")
.addEventListener("click", blsagGenerateKeys);
document
.getElementById("blsag-sign-message1")
.addEventListener("click", () => blsagSignMessage(0));
document
.getElementById("blsag-sign-message2")
.addEventListener("click", () => blsagSignMessage(1));
document
.getElementById("blsag-check-linkability")
.addEventListener("click", blsagCheckLinkability);
document
.getElementById("compact-generate-keys")
.addEventListener("click", compactGenerateKeys);
document
.getElementById("compact-sign-sag")
.addEventListener("click", compactSignSAG);
document
.getElementById("compact-sign-blsag")
.addEventListener("click", compactSignBLSAG);
document
.getElementById("compact-compare-sizes")
.addEventListener("click", compactCompareSizes);
document.querySelectorAll(".tab").forEach((tab) => {
tab.addEventListener("click", () => {
const tabName = tab.getAttribute("data-tab");
switchTab(tabName);
});
});
}
function generateKeys() {
try {
const output = document.getElementById("sag-output");
output.textContent = "Generating keys for the ring...\n";
state.keypairs = [];
for (let i = 0; i < 3; i++) {
const format = "xonly";
const keypair = wasm_generate_keypair(format);
state.keypairs.push({
privateKeyHex: keypair.private_key_hex,
publicKeyHex: keypair.public_key_hex,
});
}
state.ringPubkeys = state.keypairs.map((kp) => kp.publicKeyHex);
output.textContent += "Generated 3 keypairs:\n";
state.keypairs.forEach((kp, i) => {
output.textContent += `\nRing Member ${i + 1}:\n`;
output.textContent += `Private Key: ${kp.privateKeyHex.substring(
0,
10
)}...${kp.privateKeyHex.substring(kp.privateKeyHex.length - 10)}\n`;
output.textContent += `Public Key: ${kp.publicKeyHex.substring(
0,
10
)}...${kp.publicKeyHex.substring(kp.publicKeyHex.length - 10)}\n`;
});
output.textContent +=
'\nRing is ready for signing. Click "Sign Message" to continue.';
document.getElementById("sign-message").disabled = false;
} catch (error) {
document.getElementById("sag-output").textContent =
"Error generating keys: " + error.message;
}
}
function signMessage() {
try {
const output = document.getElementById("sag-output");
output.textContent =
"Signing message with Ring Member 2's private key...\n";
output.textContent += `Message: "${state.message}"\n\n`;
const signerIndex = 1; const signerPrivateKey = state.keypairs[signerIndex].privateKeyHex;
const encoder = new TextEncoder();
const messageBytes = encoder.encode(state.message);
state.signature = wasm_sign(
messageBytes,
signerPrivateKey,
state.ringPubkeys
);
output.textContent += "Generated Ring Signature:\n";
output.textContent += `c0: ${state.signature.c0.substring(
0,
10
)}...${state.signature.c0.substring(
state.signature.c0.length - 10
)}\n`;
output.textContent += `s values: [${state.signature.s.length} scalars]\n`;
output.textContent +=
'\nClick "Verify Signature" to verify it against the ring.';
document.getElementById("verify-signature").disabled = false;
} catch (error) {
document.getElementById("sag-output").textContent =
"Error signing message: " + error.message;
}
}
function verifySignature() {
try {
const output = document.getElementById("sag-output");
output.textContent = "Verifying signature against the ring...\n";
output.textContent += `Message: "${state.message}"\n\n`;
const encoder = new TextEncoder();
const messageBytes = encoder.encode(state.message);
const isValid = wasm_verify(
state.signature,
messageBytes,
state.ringPubkeys
);
output.textContent += `Verification result: ${
isValid ? "VALID ✓" : "INVALID ✗"
}\n`;
output.textContent +=
"\nThe signature proves that someone in the ring signed this message,";
output.textContent +=
"\nbut it doesn't reveal which specific member.";
output.textContent +=
'\n\nClick "Tamper & Verify" to see what happens if the message is modified.';
document.getElementById("tamper-message").disabled = false;
} catch (error) {
document.getElementById("sag-output").textContent =
"Error verifying signature: " + error.message;
}
}
function tamperAndVerify() {
try {
const output = document.getElementById("sag-output");
const tamperedMessage = state.message + " [TAMPERED]";
output.textContent = "Verifying signature with tampered message...\n";
output.textContent += `Original message: "${state.message}"\n`;
output.textContent += `Tampered message: "${tamperedMessage}"\n\n`;
const encoder = new TextEncoder();
const tamperedBytes = encoder.encode(tamperedMessage);
const isValid = wasm_verify(
state.signature,
tamperedBytes,
state.ringPubkeys
);
output.textContent += `Verification result: ${
isValid ? "VALID ✓" : "INVALID ✗"
}\n`;
output.textContent +=
"\nThe signature is invalid for the tampered message.";
output.textContent +=
"\nThis proves that the signature is bound to the original message.";
output.textContent +=
"\n\nDemo complete! Try the BLSAG tab for linkable signatures.";
} catch (error) {
document.getElementById("sag-output").textContent =
"Error in tamper verification: " + error.message;
}
}
function blsagGenerateKeys() {
try {
const output = document.getElementById("blsag-output");
output.textContent = "Generating keys for the BLSAG ring...\n";
blsagState.keypairs = [];
for (let i = 0; i < 4; i++) {
const format = "xonly";
const keypair = wasm_generate_keypair(format);
blsagState.keypairs.push({
privateKeyHex: keypair.private_key_hex,
publicKeyHex: keypair.public_key_hex,
});
}
blsagState.ringPubkeys = blsagState.keypairs.map(
(kp) => kp.publicKeyHex
);
output.textContent += "Generated 4 keypairs:\n";
blsagState.keypairs.forEach((kp, i) => {
output.textContent += `\nRing Member ${i + 1}:\n`;
output.textContent += `Public Key: ${kp.publicKeyHex.substring(
0,
10
)}...${kp.publicKeyHex.substring(kp.publicKeyHex.length - 10)}\n`;
});
output.textContent +=
'\nRing is ready for signing. Click "Sign First Message" to continue.';
document.getElementById("blsag-sign-message1").disabled = false;
} catch (error) {
document.getElementById("blsag-output").textContent =
"Error generating BLSAG keys: " + error.message;
}
}
function blsagSignMessage(messageIndex) {
try {
const output = document.getElementById("blsag-output");
output.textContent = `Signing message ${
messageIndex + 1
} with Ring Member 2's private key...\n`;
output.textContent += `Message: "${blsagState.messages[messageIndex]}"\n\n`;
const signerIndex = 1; const signerPrivateKey =
blsagState.keypairs[signerIndex].privateKeyHex;
const encoder = new TextEncoder();
const messageBytes = encoder.encode(
blsagState.messages[messageIndex]
);
const result = wasm_sign_blsag(
messageBytes,
signerPrivateKey,
blsagState.ringPubkeys
);
blsagState.signatures[messageIndex] = result;
blsagState.keyImages[messageIndex] = result.key_image;
output.textContent += "Generated BLSAG Signature:\n";
output.textContent += `c0: ${result.c0.substring(
0,
10
)}...${result.c0.substring(result.c0.length - 10)}\n`;
output.textContent += `Key Image: ${result.key_image.substring(
0,
10
)}...${result.key_image.substring(result.key_image.length - 10)}\n`;
const isValid = wasm_verify_blsag(
result,
messageBytes,
blsagState.ringPubkeys
);
output.textContent += `\nVerification result: ${
isValid ? "VALID ✓" : "INVALID ✗"
}\n`;
if (messageIndex === 0) {
document.getElementById("blsag-sign-message2").disabled = false;
output.textContent +=
'\nClick "Sign Second Message" to sign another message with the same key.';
} else {
document.getElementById("blsag-check-linkability").disabled = false;
output.textContent +=
'\nClick "Check Linkability" to compare the key images.';
}
} catch (error) {
document.getElementById("blsag-output").textContent =
"Error signing BLSAG message: " + error.message;
}
}
function blsagCheckLinkability() {
try {
const output = document.getElementById("blsag-output");
output.textContent =
"Checking linkability between the two signatures...\n\n";
const keyImage1 = blsagState.keyImages[0];
const keyImage2 = blsagState.keyImages[1];
output.textContent += `Key Image 1: ${keyImage1.substring(
0,
10
)}...${keyImage1.substring(keyImage1.length - 10)}\n`;
output.textContent += `Key Image 2: ${keyImage2.substring(
0,
10
)}...${keyImage2.substring(keyImage2.length - 10)}\n\n`;
const match = wasm_key_images_match(keyImage1, keyImage2);
output.textContent += `Key images match: ${
match ? "YES ✓" : "NO ✗"
}\n\n`;
output.textContent += "Explanation:\n";
output.textContent +=
"- In BLSAG, each signer produces a unique key image derived from their private key\n";
output.textContent +=
"- This key image is the same for all signatures by the same key\n";
output.textContent +=
"- We can detect that both messages were signed by the same ring member\n";
output.textContent +=
"- Yet we still don't know which specific ring member signed\n\n";
output.textContent +=
"This linkability feature is useful for preventing double-voting,\n";
output.textContent +=
"double-spending, or other scenarios where we need to detect repeated\n";
output.textContent +=
"participation while still preserving anonymity within the group.";
} catch (error) {
document.getElementById("blsag-output").textContent =
"Error checking linkability: " + error.message;
}
}
function compactGenerateKeys() {
try {
const output = document.getElementById("compact-output");
output.textContent = "Generating keys for the ring...\n";
compactState.keypairs = [];
for (let i = 0; i < 3; i++) {
const format = "xonly";
const keypair = wasm_generate_keypair(format);
compactState.keypairs.push({
privateKeyHex: keypair.private_key_hex,
publicKeyHex: keypair.public_key_hex,
});
}
compactState.ringPubkeys = compactState.keypairs.map(
(kp) => kp.publicKeyHex
);
output.textContent += "Generated 3 keypairs for the ring.\n";
output.textContent += `Signer will be Ring Member 2 (middle keypair).\n`;
output.textContent += `\nMessage to sign: "${compactState.message}"\n`;
output.textContent +=
'\nReady to sign. Click "Sign with SAG" to continue.';
document.getElementById("compact-sign-sag").disabled = false;
} catch (error) {
document.getElementById("compact-output").textContent =
"Error generating keys: " + error.message;
}
}
function compactSignSAG() {
try {
const output = document.getElementById("compact-output");
output.textContent = "Signing with both formats (SAG)...\n";
const signerIndex = 1; const signerPrivateKey =
compactState.keypairs[signerIndex].privateKeyHex;
const encoder = new TextEncoder();
const messageBytes = encoder.encode(compactState.message);
const hexSignature = wasm_sign(
messageBytes,
signerPrivateKey,
compactState.ringPubkeys
);
compactState.signatures.sag.hex = hexSignature;
const compactSignature = wasm_sign_compact_sag(
messageBytes,
signerPrivateKey,
compactState.ringPubkeys
);
compactState.signatures.sag.compact = compactSignature;
output.textContent += "\n=== SAG Signatures ===\n";
output.textContent += "\nOriginal Hex Format:\n";
output.textContent += `c0: ${hexSignature.c0.substring(
0,
15
)}...${hexSignature.c0.substring(hexSignature.c0.length - 15)}\n`;
output.textContent += `s values: ${hexSignature.s.length} scalars, each ~64 chars\n`;
output.textContent += "\nCompact Format:\n";
output.textContent += `${compactSignature}\n`;
const hexValid = wasm_verify(
hexSignature,
messageBytes,
compactState.ringPubkeys
);
const compactValid = wasm_verify_compact(
compactSignature,
messageBytes,
compactState.ringPubkeys
);
output.textContent += `\nBoth signature formats are valid: ${
hexValid && compactValid ? "✓" : "✗"
}\n`;
output.textContent +=
'\nNow try "Sign with BLSAG" to create linkable signatures.';
document.getElementById("compact-sign-blsag").disabled = false;
} catch (error) {
document.getElementById("compact-output").textContent =
"Error signing with SAG: " + error.message;
}
}
function compactSignBLSAG() {
try {
const output = document.getElementById("compact-output");
output.textContent = "Signing with both formats (BLSAG)...\n";
const signerIndex = 1; const signerPrivateKey =
compactState.keypairs[signerIndex].privateKeyHex;
const encoder = new TextEncoder();
const messageBytes = encoder.encode(compactState.message);
const hexSignature = wasm_sign_blsag(
messageBytes,
signerPrivateKey,
compactState.ringPubkeys
);
compactState.signatures.blsag.hex = hexSignature;
const compactSignature = wasm_sign_compact_blsag(
messageBytes,
signerPrivateKey,
compactState.ringPubkeys
);
compactState.signatures.blsag.compact = compactSignature;
output.textContent += "\n=== BLSAG Signatures ===\n";
output.textContent += "\nOriginal Hex Format:\n";
output.textContent += `c0: ${hexSignature.c0.substring(
0,
15
)}...${hexSignature.c0.substring(hexSignature.c0.length - 15)}\n`;
output.textContent += `Key Image: ${hexSignature.key_image.substring(
0,
15
)}...${hexSignature.key_image.substring(
hexSignature.key_image.length - 15
)}\n`;
output.textContent += `s values: ${hexSignature.s.length} scalars, each ~64 chars\n`;
output.textContent += "\nCompact Format:\n";
output.textContent += `${compactSignature}\n`;
const hexValid = wasm_verify_blsag(
hexSignature,
messageBytes,
compactState.ringPubkeys
);
const compactValid = wasm_verify_compact(
compactSignature,
messageBytes,
compactState.ringPubkeys
);
output.textContent += `\nBoth signature formats are valid: ${
hexValid && compactValid ? "✓" : "✗"
}\n`;
output.textContent +=
'\nReady to compare sizes. Click "Compare Formats" to continue.';
document.getElementById("compact-compare-sizes").disabled = false;
} catch (error) {
document.getElementById("compact-output").textContent =
"Error signing with BLSAG: " + error.message;
}
}
function compactCompareSizes() {
try {
const output = document.getElementById("compact-output");
output.textContent = "=== Size Comparison ===\n\n";
const sagInfo = wasm_get_compact_signature_info(
compactState.signatures.sag.compact
);
const blsagInfo = wasm_get_compact_signature_info(
compactState.signatures.blsag.compact
);
const sagHexSize =
compactState.signatures.sag.hex.c0.length +
compactState.signatures.sag.hex.s.reduce(
(total, s) => total + s.length,
0
);
const blsagHexSize =
compactState.signatures.blsag.hex.c0.length +
compactState.signatures.blsag.hex.s.reduce(
(total, s) => total + s.length,
0
) +
compactState.signatures.blsag.hex.key_image.length;
output.textContent += "SAG (Unlinkable) Signature:\n";
output.textContent += ` Original Hex Format: ~${sagHexSize} characters\n`;
output.textContent += ` Compact Format: ${sagInfo.size} bytes\n`;
output.textContent += ` Size Reduction: ${Math.round(
((sagHexSize - sagInfo.size) / sagHexSize) * 100
)}%\n\n`;
output.textContent += "BLSAG (Linkable) Signature:\n";
output.textContent += ` Original Hex Format: ~${blsagHexSize} characters\n`;
output.textContent += ` Compact Format: ${blsagInfo.size} bytes\n`;
output.textContent += ` Size Reduction: ${Math.round(
((blsagHexSize - blsagInfo.size) / blsagHexSize) * 100
)}%\n\n`;
output.textContent += "=== How Compact Format Works ===\n\n";
output.textContent += "The compact format ('ringA' prefix) uses:\n";
output.textContent +=
"1. CBOR binary encoding for compact representation\n";
output.textContent +=
"2. Single-character field names (v, c, s, i)\n";
output.textContent +=
"3. Binary storage of scalars and curve points\n";
output.textContent +=
"4. base64url encoding for text transmission\n\n";
output.textContent += "Variant field indicates signature type:\n";
output.textContent += ` SAG signature: "${sagInfo.variant}"\n`;
output.textContent += ` BLSAG signature: "${blsagInfo.variant}"\n\n`;
output.textContent +=
"BLSAG signatures are larger but enable linkability detection.";
} catch (error) {
document.getElementById("compact-output").textContent =
"Error comparing sizes: " + error.message;
}
}
function switchTab(tabName) {
document.querySelectorAll(".tab-content").forEach((tab) => {
tab.classList.remove("active");
});
document.getElementById(tabName).classList.add("active");
document.querySelectorAll(".tab").forEach((tab) => {
tab.classList.remove("active");
});
document
.querySelector(`.tab[data-tab="${tabName}"]`)
.classList.add("active");
}
initWasm();
</script>
</body>
</html>