{"_id":"@condorcet.vote/crypto-vote","_rev":"10-0f75efbc314385cd721b1092c48ebb87","name":"@condorcet.vote/crypto-vote","dist-tags":{"latest":"1.0.0"},"versions":{"0.0.7":{"name":"@condorcet.vote/crypto-vote","version":"0.0.7","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.0.7","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"dist":{"shasum":"fbe247da1289b13c5f5516e0e75cb073ebabff2e","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.0.7.tgz","fileCount":7,"integrity":"sha512-wszE66Xk0s2QuFGgQ1NYs9YhpGxsJlL5z+v0Sq3fv4K+wdTsssd3z/PzxN+QYTj1grSmbuQqNs0PCBtTQtaolQ==","signatures":[{"sig":"MEUCIGglOVLMJwYLgtSsLez8w8fCJB7o973EqWRoSpnmUzqBAiEAnbbpsgvjOprfEjeXX+hyUMGkIQ51BQINNURMdtPCoTg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":293024},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","gitHead":"be3ec1e89bf36277cbab029ecda9790d6af32512","_npmUser":{"name":"julien-boudry","email":"julien.boudry@proton.me"},"_npmVersion":"11.12.1","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./crypto_vote.js","./snippets/*"],"_nodeVersion":"25.9.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.0.7_1782203886648_0.6575601771245989","host":"s3://npm-registry-packages-npm-production"}},"0.0.9":{"name":"@condorcet.vote/crypto-vote","version":"0.0.9","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.0.9","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"e6056eba2fa774f38a931425614ac1f4ee1230dc","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.0.9.tgz","fileCount":7,"integrity":"sha512-vTwIbq1A5xzWt/lgexpLTNRd9UvL17EZr/wPhMcOrLNUDOhz/+zEl3vjmx7tI1xn3vgZ6CK6Ksxg8/2B1QoxkA==","signatures":[{"sig":"MEUCIQCanOQI9pH57ijT/r5MJpG2GRK1jLYRJxdNIlKda3z7FQIge/nkK2pVg8cvbC3YiOHI/gu2hPaN1ks0Vy3luARmArw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.0.9","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":294311},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.13.0","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./crypto_vote.js","./snippets/*"],"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.0.9_1782207908623_0.451505732628412","host":"s3://npm-registry-packages-npm-production"}},"0.0.10":{"name":"@condorcet.vote/crypto-vote","version":"0.0.10","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.0.10","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"ef0e6f57ef02eee3d49f63943cf186b66091fca1","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.0.10.tgz","fileCount":6,"integrity":"sha512-AYVNPiq/w+xwcWfaQYAc06rLwUqzv3G6TytKzSyK/C5zFgATP1PoH4P2qv6pM8s1lPTfH0aMAU8wy476Z7Ik3g==","signatures":[{"sig":"MEUCIFxqgNRAd0TKVqLLa2aBXnMiC38vkKx6AmwGJ0GQTuSRAiEA4juVGCo0/GBncNRZW9/QFNahK2SBzlQALTjZjB2L2Pw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.0.10","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":299995},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.13.0","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.0.10_1782210530074_0.5582759915812612","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@condorcet.vote/crypto-vote","version":"0.1.0","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.1.0","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"93a2f4bb04d397ebeb2154bf1ba89ec332e3b563","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.1.0.tgz","fileCount":6,"integrity":"sha512-lCl1PY4wtVmG1k9AROEP+6eI48jIdrxcQEC2Mkz7tAzRb+vUvoOPHjlQqUUJDDF4p016LZUPCJn/DtcKtgF42Q==","signatures":[{"sig":"MEUCIAneLvwmwOH1cnPrX+IerjqVgyjLvPazKeDhhSliloygAiEAivqzVDqayrxpsFomFM2zX9O3sSYkLISVrz47WJ+quow=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":319447},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.13.0","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.1.0_1782222675910_0.397594562419614","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@condorcet.vote/crypto-vote","version":"0.1.1","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.1.1","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"cac4af6acb501ca7942b1ca602c5724284e0a4e0","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.1.1.tgz","fileCount":6,"integrity":"sha512-nzr2A8qnzobGA5v5tdeyjApKOMrLjYx9Gbc1nmEXQIamwTIhG3ZWNX7zs0RAfPelodkYQdPRnLlfTaUuElrGPQ==","signatures":[{"sig":"MEYCIQDwnixTqXrCzjtKD8wN15WdF8eXFI2nx5zCO+IZYgg6AAIhANIZydfqbXnKH8o/A3nifMAEqvwEiLOJVHOqrq6OlvQ4","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":319447},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.13.0","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.1.1_1782223129093_0.21035748569154555","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@condorcet.vote/crypto-vote","version":"0.2.0","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.2.0","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"d2f7bee3394705e89ef1880d824ae2ac19aa9ec4","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.2.0.tgz","fileCount":6,"integrity":"sha512-eXCDo1QYaDnjEmfqrjanB1cx0qcJZFeLzjsGYAafan7HRZIl8wfZ1E/CgCDf1Bua9tyk2eX/OQOFrvyhnbI4Ow==","signatures":[{"sig":"MEUCIGeSruNH1GEgdk9+m6+Dx2J2EgwHcrQzSxVCPH4GcwfwAiEAnJeXt2Gh3+M4BkF6wWyKNoTgXv+kZA9oDv1HfRR1jU8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":330628},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.13.0","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.2.0_1782224048732_0.7848765661284223","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@condorcet.vote/crypto-vote","version":"0.3.0","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.3.0","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"1b89d66b20a329679652083595cbf4492dfdfe3c","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.3.0.tgz","fileCount":6,"integrity":"sha512-x7A6HWkTGULQcVUGSNc2hJrlRDyTb7y0LZG0A3ooYDvnZsHk3XH+4vbB25OwOVQQF0p2CrMhOCi+p2SAp8oLqQ==","signatures":[{"sig":"MEUCIBcUrmp135ELwDrfNVbHfYj/FvXuUVz9pXLZxUQ3gkKvAiEA5MATJTYHWhXJpPDo63mVWMhKAigAZ1Jf3TBY8ms6hnI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":350714},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.13.0","description":"An agnostic cryptographic oracle for verifiable voting using linkable ring signatures (BLSAG) over Ristretto255, with Blake2b-512 as the hash function.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.16.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.3.0_1782228909124_0.16872315795186488","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@condorcet.vote/crypto-vote","version":"0.4.0","keywords":["voting","vote","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.4.0","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"c189b9238ed6ba128658893882811421e911558e","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.4.0.tgz","fileCount":6,"integrity":"sha512-PhSDLui/ID9OsAXIrn0kXjIlOlmXH4BK3Kcyu/8mSQbB3F7N2a907CWr/luw2Wde/6ORxDDyaezI1XB6Ycbxkg==","signatures":[{"sig":"MEUCIQCN0VkANL/TTvQuxNQdWFiCVLKfAcX6GCJBjL89+28zKAIgHVHii6nJGexOJR0lIkPePY+mJO0vEPjkBBmpR30jZyY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":350263},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.16.0","description":"Sign and verify anonymous, double-vote-resistant ballots with linkable BLSAG ring signatures over Ristretto255 + Blake2b-512. Native CLI and WebAssembly (browser + WASI) builds.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.18.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.4.0_1786256157801_0.25091762343059276","host":"s3://npm-registry-packages-npm-production"}},"0.4.2":{"name":"@condorcet.vote/crypto-vote","version":"0.4.2","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"license":"AGPL-3.0-or-later","_id":"@condorcet.vote/crypto-vote@0.4.2","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"2cf0d7a41d5d73e88b562f45db73bf1486afc30e","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-0.4.2.tgz","fileCount":6,"integrity":"sha512-/wQPe0RiJMCCre+oO1z69brrW3zf+bUHZSy0u5SaZEtv6DV89zkeu8ul+n8EAiD90wZa3/HEZjZ41zzsq+lPEQ==","signatures":[{"sig":"MEQCIH82gGY8JhxypI7EoKVRRrTT620JEkoL1NVbX432y9QyAiBQFqs0bWl0Bcs/v/4aOXu0bv6ekVLVdVK08NkrthAp0w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@0.4.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":350755},"main":"crypto_vote.js","type":"module","types":"crypto_vote.d.ts","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.16.0","description":"Sign and verify anonymous, double-vote-resistant ballots with linkable BLSAG ring signatures over Ristretto255 + Blake2b-512. Native CLI and WebAssembly (browser + WASI) builds.","directories":{},"sideEffects":["./snippets/*"],"_nodeVersion":"24.18.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/crypto-vote_0.4.2_1786258117795_0.7606415854658732","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"_id":"@condorcet.vote/crypto-vote@1.0.0","bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"dist":{"shasum":"20c4cd63dcfbc107d8cc8d3c4d7be00258d0cfe4","tarball":"https://registry.npmjs.org/@condorcet.vote/crypto-vote/-/crypto-vote-1.0.0.tgz","fileCount":6,"integrity":"sha512-QFcePKdwdPoVPY3s58kyIA3thsYMSpl23jQi/zV5+ZA9szwomMUp7lGTfXQ5REjYbiFRfonsp5YKjTwVRxwbPQ==","signatures":[{"sig":"MEUCIQCFk/Cf/aO318Y0oeo0TKQ/sJwcIXhzOPZ20mVre47+pgIgIJ0pWE+3BoTJQMgkau2PV4v0LHH64xw4+RbIDHukWTU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIAsQbzI4R+c3MeiWNUbQIdjdKw67ioyyMn2tyxBu1ef5AiEA10Qv9DdGYV5xnyeFls+VQSyPVazarD1KJvOP1AgZjKM="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@condorcet.vote%2fcrypto-vote@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":368601},"main":"crypto_vote.js","name":"@condorcet.vote/crypto-vote","type":"module","types":"crypto_vote.d.ts","license":"AGPL-3.0-or-later","version":"1.0.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9792b1f2-584c-417d-886e-302382d3b459"}},"homepage":"https://github.com/CondorcetVote/cryptoVote#readme","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"_npmVersion":"11.19.0","description":"Sign and verify anonymous, double-vote-resistant ballots with linkable BLSAG ring signatures over Ristretto255 + Blake2b-512. Native CLI and WebAssembly (browser + WASI) builds.","directories":{},"maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"sideEffects":["./snippets/*"],"_nodeVersion":"24.21.0","_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/crypto-vote_1.0.0_1790342861976_0.6553816221491717"}}},"time":{"created":"2026-06-23T08:38:06.469Z","modified":"2026-09-25T13:27:42.400Z","0.0.7":"2026-06-23T08:38:06.779Z","0.0.9":"2026-06-23T09:45:08.799Z","0.0.10":"2026-06-23T10:28:50.201Z","0.1.0":"2026-06-23T13:51:16.066Z","0.1.1":"2026-06-23T13:58:49.286Z","0.2.0":"2026-06-23T14:14:08.979Z","0.3.0":"2026-06-23T15:35:09.285Z","0.4.0":"2026-08-09T06:15:58.000Z","0.4.2":"2026-08-09T06:48:37.945Z","1.0.0":"2026-09-25T13:27:42.056Z"},"bugs":{"url":"https://github.com/CondorcetVote/cryptoVote/issues"},"license":"AGPL-3.0-or-later","homepage":"https://github.com/CondorcetVote/cryptoVote#readme","keywords":["voting","cryptography","ring-signature","blsag","blake2"],"repository":{"url":"git+https://github.com/CondorcetVote/cryptoVote.git","type":"git"},"description":"Sign and verify anonymous, double-vote-resistant ballots with linkable BLSAG ring signatures over Ristretto255 + Blake2b-512. Native CLI and WebAssembly (browser + WASI) builds.","maintainers":[{"name":"julien-boudry","email":"julien.boudry@proton.me"}],"readme":"# cryptoVote\n\n[![crates.io](https://img.shields.io/crates/v/crypto-vote?logo=rust&label=crates.io)](https://crates.io/crates/crypto-vote)\n[![docs.rs](https://img.shields.io/docsrs/crypto-vote?logo=docsdotrs&label=docs.rs)](https://docs.rs/crypto-vote)\n[![npm](https://img.shields.io/npm/v/@condorcet.vote/crypto-vote?logo=npm&label=npm)](https://www.npmjs.com/package/@condorcet.vote/crypto-vote)\n[![license](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue)](LICENSE)\n\n> [!NOTE]\n> **Not independently audited.** The implementation, its test surface and\n> its threat model have been reviewed by the author with AI assistance,\n> and are backed by unit and integration tests, fuzz targets and frozen\n> protocol vectors that pin the wire format across versions. No outside\n> cryptographer has examined them yet. The linkable ring signature is\n> standard BLSAG; the only deviation is that the linking tag's base is\n> scoped by the election identifier, `I_e = x · H_p(domain ‖ election_id ‖ P)`,\n> which is the *event-oriented linkability* notion from the linkable ring\n> signature literature (Liu–Wei–Wong 2004; Tsang–Wei 2005 for e-voting)\n> applied to the per-key base. For an election with a motivated adversary\n> or legal weight, read the [threat model](#threat-model) and commission a\n> review first.\n\nA pure-Rust cryptographic oracle for **verifiable, anonymous, double-vote-resistant** ballots.\n\nThe crate is a minimal building block that offers a small set of\noperations and refuses to take part in anything else.\n\n| # | Operation | Where it runs | Function |\n|---|-----------|---------------|----------|\n| A | Generate identity | Either side | `generate_identity()` |\n| B | Sign a ballot | Voter's device (WebAssembly in the browser) | `sign_vote(secret, vote, election_id, ring)` |\n| C | Validate a proof | Host / server | `verify_vote(vote, election_id, signature, key_image, ring)` |\n| D | Prove ownership of a key image | Voter's device (prove) / anyone (verify) | `prove_ownership(secret, election_id, context)` / `verify_ownership(public, key_image, election_id, context, proof)` |\n\nOperations A–C are the core anonymous-voting flow. **Operation D is\noptional and opt-in** — it is the deliberate *inverse* of the ring\nsignature's anonymity. See [Proving ownership of a\nvote](#proving-ownership-of-a-vote-operation-d).\n\n## Cryptographic choices\n\n- **Curve**: Ristretto255 (`curve25519-dalek`). Prime-order, constant-time, pure Rust.\n- **Ring signature scheme**: BLSAG (Back's Linkable Spontaneous\n  Anonymous Group) implemented locally from the LSAG/BLSAG equations,\n  with election-scoped key images. The signer's randomness is *hedged*:\n  derived from the secret key, the message and fresh CSPRNG output\n  together, so a faulty RNG cannot repeat a nonce and leak the key.\n- **Hash**: Blake2b-512 (`blake2` crate). Natively 64-byte output, fed\n  directly into `Scalar::from_hash`.\n- **CSPRNG**: `SysRng`, which on `wasm32` delegates to `Crypto.getRandomValues`\n  through the `getrandom` crate's `wasm_js` feature.\n\n## Threat model\n\nThe library is a mathematical oracle. It guarantees exactly three things,\nand each one rests on an assumption the host must uphold:\n\n| Guarantee | Holds against | Assumes |\n|---|---|---|\n| **Unforgeability** — no valid proof without a secret key from the ring | anyone, including the host | discrete log on Ristretto255, Blake2b as a random oracle |\n| **Linkability** — one key image per `(secret key, election_id)` | anyone, including the host | the registrar enrolled **one identity per person** |\n| **Anonymity** — a proof does not reveal which ring member signed | the public, other voters, auditors | the host served **every voter the same ring and `election_id`** |\n\nWhat this means in practice:\n\n- **The host is trusted for anonymity, not just for eligibility.** A\n  ballot is anonymous within *the ring it was signed under*. A host that\n  hands each voter a slightly different ring (one decoy swapped, one\n  member missing) or a slightly different `election_id` (trailing space,\n  different suffix) can later tell whose ballot is whose by checking which\n  variant verifies. The cryptography cannot detect this; publication can.\n  Publish the frozen ring's [`ring_digest`](#checking-the-ring-you-were-served)\n  where voters can see it, and have the voting page refuse to sign when\n  the ring it fetched does not match.\n- **Not receipt-free, not coercion-resistant.** Any voter can prove how\n  they voted — with [Operation D](#proving-ownership-of-a-vote-operation-d),\n  or simply by handing over their secret key — and ballots are stored in\n  clear next to their key image. This is inherent to linkable ring\n  signatures and to verifiable receipts in general: vote buying and\n  coercion are *possible* by design. Use this scheme where verifiability\n  matters more than receipt-freeness (associations, boards, proxy votes),\n  not where voters may be pressured.\n- **One identity per person is the registrar's job.** A person enrolled\n  with two keys votes twice with two distinct key images and the library\n  cannot tell. Sybil resistance lives entirely in the enrolment process.\n- **Bound the ballot size.** Verification hashes the full ballot once\n  per ring member, so its cost is `O(ring size × ballot size)`. The\n  library imposes no limit; the host must (a few kilobytes is plenty for\n  any realistic ballot), otherwise an attacker can submit oversized\n  invalid ballots to burn verifier CPU.\n- **Use globally unique election identifiers.** If identities are reused\n  across organisations and two elections share an `election_id` string,\n  their key images coincide. A UUID avoids the question.\n- **What is *not* hidden:** the ring (who was allowed to vote), the number\n  of ballots cast, and every ballot's content. Only the mapping from\n  ballot to voter is.\n\n## Anti-double-vote contract\n\nThe module never tells the host whether a voter has already voted.\nThat decision belongs to the host. The protocol gives the host one\ndeterministic identifier per `(secret key, election_id)` pair — the\n**key image** — which it must store and de-duplicate against:\n\n1. Retrieve the key image returned by `sign_vote`.\n2. Look it up in the host's storage.\n3. Reject the transaction if it already exists.\n4. Otherwise ask `verify_vote` whether the proof is mathematically valid,\n   and on success store the key image.\n\n### Election binding\n\nThe key image is a function of both the secret key and the\n`election_id`. The same voter using the same secret key twice in one\nelection produces the same tag, so double voting is detectable; the\nsame voter using that key in another election produces a different tag,\nso public proofs are not linkable across elections just by comparing\nkey images.\n\nEvery call to `sign_vote` and `verify_vote` also mixes the same\n`election_id` byte string into the BLSAG challenge chain. The host\nshould pass a stable per-election identifier (UUID, slug, hash of the\nevent configuration, …) on both sides.\n\nThis gives two election-context checks:\n\n- The **key image** is itself scoped by `election_id`.\n- The **signature** itself only validates against the `election_id`\n  it was produced with.\n\nThe library normalises `election_id` to **Unicode NFC** before\nhashing, on both the signing and the verification side. Callers can\ntherefore pass the same logical identifier in any Unicode form\n(NFC, NFD, mixed) without breaking verification — the typical case\nwhere a server stores the ID in one normalisation and the voter's\npage receives it in another no longer silently invalidates every\nballot. ASCII identifiers are NFC by definition, so UUIDs and slugs\nare unaffected.\n\n### Ring size and the anonymity set\n\nThe cryptography guarantees the verifier cannot tell **which** member of\nthe ring produced a given signature — but the anonymity set is exactly\nthe ring. A ring of size *n* gives a `1/n` chance of guessing the signer\nuniformly at random, and **no more**:\n\n- **n = 2**: the protocol still validates, but the \"anonymity\" is\n  binary — every ballot leaks down to \"voter A or voter B\". The\n  library accepts it because cryptographically it is sound, not\n  because it is privacy-meaningful. Treat it as a debugging\n  configuration, not a production one.\n- **n < 8**: real-world side-channels (registration order, login\n  timing, IP correlation on the host) usually let an observer narrow\n  the set further. Treat anything below 8 as practically\n  de-anonymising.\n- **n ≥ 16**: a reasonable floor for a real ballot. Larger rings cost\n  more (`O(n)` for both signing and verification, plus signature size\n  of `32 * (1 + n)` bytes), so pick the largest ring your latency\n  budget allows.\n\nThe minimal `n = 2` floor is enforced inside the library; **picking a\nuseful n is the host's responsibility**.\n\n### Ring lifecycle: freeze before voting opens\n\nThe full set of authorised public keys (the \"ring\") is mixed into the\nhash chain of every signature, the same way `election_id` and the\nballot bytes are. As a consequence, the ring **must be frozen before\nthe first ballot is cast** and stay composition-identical for the\nwhole duration of the election. In practice:\n\n- **All voter identities must be generated and registered before\n  voting opens.** The natural moment is during the election's\n  enrolment window, which closes when voting opens.\n- Once voting opens, the host serves the same ring to every voter and\n  uses that same ring at `verify_vote` time. Adding, removing, or\n  swapping a member instantly invalidates every signature produced\n  under the previous ring — there is no migration path.\n- Ring **order** does not matter (both sides canonicalise it\n  internally by sorting lexicographically on the compressed point\n  encoding), so the host is free to return it in any order over the\n  wire. Only the *set* of members matters.\n- If a voter is added later, treat it as a new election: new\n  `election_id`, fresh ring, fresh key-image store. Ballots from the\n  old election remain verifiable as long as the host keeps a snapshot\n  of the old ring.\n\nFor the same reason, the host should persist the ring it used to\nverify each ballot (or at least the election's frozen ring) alongside\nthe ballot itself, so audits later on can rerun `verify_vote`\ndeterministically.\n\n### Checking the ring you were served\n\nBecause anonymity is exactly \"which ring did I sign under\", the voter's\ndevice should verify that the ring it fetched from the host is the ring\nthe election published. `ring_digest` gives a canonical fingerprint of a\nring: a pure function of the *set* of members (order does not matter,\nduplicates and rings of fewer than two keys are rejected, exactly like\nsigning).\n\n```rust\nuse crypto_vote::{generate_identity, ring_digest, RingDigest};\n\nlet ring = vec![generate_identity().public_key, generate_identity().public_key];\n\n// The host publishes this next to the frozen ring (bulletin board,\n// election page, signed announcement …):\nlet published = ring_digest(&ring).unwrap().to_prefixed();   // \"ring_…_…\"\n\n// The voter's device recomputes it from the ring it fetched and refuses\n// to sign on a mismatch.\nlet fetched = ring.iter().rev().cloned().collect::<Vec<_>>(); // any order\nassert_eq!(ring_digest(&fetched).unwrap(), RingDigest::from_prefixed(&published).unwrap());\n```\n\nThe digest is `Blake2b-512(domain ‖ n ‖ sorted keys)[..32]`, published in\nthe prefixed form `ring_<64 hex>_<8 hex checksum>`. Every front end exposes\nit: `ring_digest_wasm(ring)` and `ring_matches_digest_wasm(ring, expected)`\nin the browser, the `ring_digest` Extism function, and `cryptovote\nring-digest --ring ring.txt` on the CLI.\n\n## Proving ownership of a vote (Operation D)\n\nThe ring signature deliberately hides *which* authorised voter cast a\nballot. Operation D is the **opt-in inverse**: it lets the holder of a\nsecret key prove to a third party that a given key image — and therefore\nthe ballot next to it in the public registry — is theirs, **without\nrevealing the secret key**.\n\nThe intended use case is **mandated / proxy voting**: a voter (or a\nmandate-holder voting on someone's behalf) must be able to demonstrate,\nafter the fact, how a ballot was cast.\n\n```rust\nuse crypto_vote::{\n    generate_identity, sign_vote, generate_nonce, prove_ownership, verify_ownership,\n};\n\nlet voter   = generate_identity();\nlet other   = generate_identity();\nlet ring    = vec![voter.public_key, other.public_key];\nlet election_id = \"550e8400-e29b-41d4-a716-446655440000\";\n\n// The voter casts a ballot as usual.\nlet vote = sign_vote(&voter.secret_key, b\"option-A\", election_id, &ring).unwrap();\n\n// Later, a verifier (possibly external to the election) generates a fresh\n// nonce and sends it to the voter, who proves the registry's key image is\n// theirs. `context` is opaque bytes — here, the nonce's raw bytes.\nlet nonce = generate_nonce();\nlet proof = prove_ownership(&voter.secret_key, election_id, nonce.as_bytes());\n\n// The verifier checks it with public data only — the voter's public key,\n// the key image from the registry, the election id and the same nonce.\nassert!(verify_ownership(\n    &voter.public_key,\n    &vote.key_image,\n    election_id,\n    nonce.as_bytes(),\n    &proof,\n));\n```\n\n### What it is\n\nA non-interactive **Chaum–Pedersen proof of equality of discrete\nlogarithms**. The key image is `I = x·B`, where `B = H_p(election_id || P)`\nis the same election-scoped base the key image was built from and\n`P = x·G` is the voter's public key. The proof demonstrates knowledge of\na single scalar `x` satisfying **both** `P = x·G` and `I = x·B` — which\nonly the true owner of `I` knows — while revealing nothing else about `x`.\nOn the wire it is 64 bytes (`own_<128 hex>_<8 hex checksum>` in the\nprefixed format), independent of the ring size.\n\n### The verifier can be anyone, with their own nonce\n\nVerification needs **only public data**: the voter's public key, the key\nimage, the `election_id` and the proof. No cooperation from the\norganisers and no secret are required, so a party completely external to\nthe election can check a proof on its own.\n\nEverything the verifier wants the proof bound to goes into `context`. The\nrecommended pattern is a **verifier-chosen nonce**:\n\n1. the verifier picks a fresh random nonce and sends it to the prover;\n2. the prover calls `prove_ownership` with `context = nonce` (the service\n   may also append the ballot bytes);\n3. the verifier checks with the same `context`.\n\nThe nonce is folded into the proof's challenge, so the prover could not\nhave precomputed it — the proof is **fresh** and cannot be replayed.\n\n`generate_nonce()` is provided as a verifier-side convenience: it returns\na `Nonce` (32 fresh bytes) from the same platform CSPRNG (`SysRng`; Web\nCrypto in the browser). The organisation requesting the proof calls it,\nsends the nonce to the prover, and both pass its bytes as `context`. Using\nit is optional — any unpredictable, single-use value works.\n\nLike every other public value, a `Nonce` has the full encoding surface,\nincluding the self-describing, checksum-protected prefixed form\n(`nonce_<hex>_<checksum>`). On the high-level bindings the nonce travels\n**exclusively** in that form (validated on the way in), exactly like keys,\nsignatures and the proof itself — the prefix is transport only, the proof\nbinds the nonce's raw bytes. The pure-Rust API stays fully general: it\ntakes opaque `context: &[u8]`, so a richer context (e.g. the nonce with\nthe ballot bytes appended) is always available there.\n\n### Properties and limits\n\n- **No secret key is revealed.** The proof is zero-knowledge for `x`.\n- **Sound.** A third party cannot prove ownership of a key image that is\n  not theirs (they would have to know the matching secret scalar).\n- **De-anonymising by design.** Producing a proof intentionally ties\n  `P ↔ I`; it is the opt-in opposite of the ring signature's anonymity.\n- **Transferable, *not* designated-verifier.** A Chaum–Pedersen proof is\n  publicly checkable, so the nonce gives *freshness* but **not**\n  non-transferability: whoever holds `(context, proof)` can convince\n  anyone else too. For ordinary mandate scenarios this is fine. If you\n  need a proof that convinces *only* the designated verifier, you need a\n  different (designated-verifier) construction — this crate does not\n  provide one.\n- **It does not consult any registry.** `verify_ownership` answers exactly\n  \"does the holder of this public key vouch for this key image, under this\n  election and context?\" Tying the key image to a specific ballot is the\n  registry's job (the vote signature binds ballot + key image, and the\n  host de-duplicates on the key image); the caller does that lookup.\n- **Coercion note.** This makes the \"prove how I voted\" capability\n  concrete and transferable. It is the right tool for mandated voting, but\n  by the same token it means a coercer who can compel a proof (or the\n  secret key) can learn how someone voted. This is an inherent property of\n  *verifiable* receipts, not a flaw in the proof.\n\n### From JavaScript / Extism\n\nEvery value crossing these boundaries — including the nonce and the proof\n— uses the prefixed, checksum-protected form (`nonce_…`, `own_…`, `pk_…`,\n`ki_…`), validated on input.\n\n- **wasm-bindgen:** `generate_nonce_wasm()` returns a fresh prefixed nonce\n  string (`nonce_…`); `prove_ownership_wasm(secret, electionId, nonce)`\n  returns the prefixed `own_…` proof; `verify_ownership_wasm(publicKey,\n  keyImage, electionId, nonce, proof)` returns a `bool`.\n- **Extism:** `generate_nonce` takes no input and returns\n  `{\"nonce\": nonce_…}`; `prove_ownership` takes\n  `{\"secret\": sk_…, \"election_id\": str, \"nonce\": nonce_…}` and returns\n  `{\"proof\": own_…}`; `verify_ownership` takes\n  `{\"public\": pk_…, \"key_image\": ki_…, \"election_id\": str, \"nonce\": nonce_…, \"proof\": own_…}`\n  and returns `{\"valid\": bool}`.\n\nFor a custom or binary context (e.g. nonce + ballot bytes), use the\npure-Rust API, which takes opaque `context: &[u8]`.\n\n## Build matrix\n\nThe release workflow produces eight artefacts; the same commands work\nlocally. Pick the triple/flavour that matches your need, install its\none-off prerequisite, then run the matching build command. Edition 2024\nimplies Rust ≥ 1.85; CI tracks `stable`.\n\n| Triple — flavour | Build host | One-off setup | Output |\n|---|---|---|---|\n| `x86_64-unknown-linux-gnu` | Linux x64 | system C linker (any dev box has one) | dynamic ELF, ~700 KB |\n| `x86_64-unknown-linux-musl` | Linux x64 | `musl-tools` (provides `musl-gcc`) | **static ELF**, ~800 KB |\n| `aarch64-unknown-linux-gnu` | Linux x64 or arm | `gcc-aarch64-linux-gnu` cross-toolchain | dynamic ELF |\n| `aarch64-unknown-linux-musl` | Linux x64 or arm | none — uses `rust-lld` | **static ELF** |\n| `riscv64gc-unknown-linux-gnu` | Linux x64 or arm | `gcc-riscv64-linux-gnu` cross-toolchain | dynamic ELF |\n| `aarch64-apple-darwin` | macOS arm (M-series) | Xcode Command Line Tools | Mach-O |\n| `wasm32-unknown-unknown` — wasm-bindgen | any | `cargo install wasm-pack` | ES-module bundle for browsers |\n| `wasm32-wasip1` — **Extism plugin** | any | none — uses `rust-lld` | single `.wasm` for every Extism host SDK |\n\n> Install commands below are **Debian/Ubuntu** (`apt`); adapt to your\n> distribution (`dnf`, `pacman`, `zypper`, `brew`, …) or use\n> [`cross`](https://github.com/cross-rs/cross) for a Docker-based path\n> that needs nothing on the host. CI runs on `ubuntu-latest`, which is\n> why the upstream pipeline uses apt.\n\n### Build commands\n\nStandard template — works as-is for `x86_64-unknown-linux-gnu`,\n`x86_64-unknown-linux-musl` (after `apt install musl-tools`) and\n`aarch64-apple-darwin` (on a macOS host):\n\n```bash\ncargo build --release --locked --target <TRIPLE> --bin cryptovote\n# → target/<TRIPLE>/release/cryptovote\n```\n\nThree cross-Linux rows need a linker selector — set it in the\nenvironment, then run the same command:\n\n```bash\n# aarch64-unknown-linux-gnu  (after `apt install gcc-aarch64-linux-gnu`)\nexport CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc\n\n# aarch64-unknown-linux-musl  (no apt package needed)\nexport CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=rust-lld\n\n# riscv64gc-unknown-linux-gnu  (after `apt install gcc-riscv64-linux-gnu`)\nexport CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc\n```\n\nBrowser WASM uses `wasm-pack` (which wraps `cargo build` and emits JS\nglue alongside the `.wasm`). The `wasm-release` profile is the\nWebAssembly-tuned variant defined in `Cargo.toml`: same size knobs as\n`release`, plus `panic = \"abort\"` (see Cargo.toml's `[profile.release]`\ncomment for why panic strategy is kept out of the default release\nprofile):\n\n```bash\nwasm-pack build --profile wasm-release --target web --out-dir pkg-browser \\\n    -- --no-default-features --features wasm --locked\n# → pkg-browser/{crypto_vote.js, crypto_vote_bg.wasm, …}\n```\n\n`--target web` emits a self-contained ES module you can `import` from a\nstatic page, a bundler (Vite, Rollup, esbuild), Bun, or Node — it\ninstantiates the WASM itself via an async `init()`, so no bundler plugin\nis required anywhere. This is the build published to npm.\n`--no-default-features` drops `clap`; `--features wasm` enables\n`wasm-bindgen`, `js-sys`, and the `getrandom/wasm_js` backend so\nrandomness comes from `Crypto.getRandomValues`.\n\nServer-side WASM uses the Extism flavour — the same `.wasm` is then\nloadable by every Extism host SDK (browser, Node, Python, Go, Rust, …).\n`getrandom` autoselects WASI's `random_get` on this target, so no extra\nbackend wiring is needed:\n\n```bash\ncargo build --profile wasm-release --locked --target wasm32-wasip1 --lib --no-default-features --features extism\n# → target/wasm32-wasip1/wasm-release/crypto_vote.wasm   (~360 KB)\n```\n\nSee [Extism plugin](#extism-plugin) for the host-side usage from JS,\nNode, and other Extism SDKs.\n\n## Using the library\n\n```rust\nuse crypto_vote::{generate_identity, sign_vote, verify_vote};\n\nlet alice   = generate_identity();\nlet bob     = generate_identity();\nlet charlie = generate_identity();\n\nlet ring        = vec![alice.public_key, bob.public_key, charlie.public_key];\nlet election_id = \"550e8400-e29b-41d4-a716-446655440000\"; // any stable string (UUID, slug, …)\n\n// Bob signs \"option-A\" — `bob.secret_key` never leaves Bob's device.\nlet proof = sign_vote(&bob.secret_key, b\"option-A\", election_id, &ring).unwrap();\n\n// The host: first dedup `proof.key_image`, then verify.\nassert!(verify_vote(\n    b\"option-A\",\n    election_id,\n    &proof.signature,\n    &proof.key_image,\n    &ring,\n));\n```\n\n## Encoding formats\n\nEvery value the API exchanges as text — public keys, secret keys, key\nimages, signatures — has **two interchangeable string encodings**. They\ncarry the exact same bytes; nothing about the cryptography changes\nbetween them.\n\n| Format | Example | Methods |\n|---|---|---|\n| **Bare hex** | `e2f2ae0a…2d76` | `to_hex()` / `from_hex(..)` |\n| **Prefixed** | `pk_e2f2ae0a…2d76_bfb1c73d` | `to_prefixed()` / `from_prefixed(..)` |\n\nThe **prefixed** format wraps the same lowercase hex body with two\nconveniences:\n\n```text\n  pk_e2f2ae0a…e08d2d76_bfb1c73d\n  │  │                 │\n  │  │                 └ checksum: 4 bytes (8 hex chars)\n  │  └ body: identical to to_hex()\n  └ tag: pk | sk | ki | blsag | own | nonce | ring\n```\n\n- a **tag** up front (`pk_`, `sk_`, `ki_`, `blsag_`, `own_`, `nonce_`,\n  `ring_`) says what kind of\n  value it is, so a public key pasted where a key image was expected is\n  caught immediately instead of failing deep in verification;\n- a **checksum** at the end (a 4-byte BLAKE3 digest, hex-encoded) catches\n  the overwhelming majority of single-character typos, transpositions and\n  truncated pastes. The tag is folded into the checksum, so relabelling a\n  `pk_…` value as `ki_…` is also detected.\n\nThe checksum is an **integrity / typo guard only — never a security\nprimitive**. Anyone can compute a valid checksum for any bytes;\nauthenticity comes solely from the BLSAG proof.\n\n| Type | Tag | `from_prefixed` signature |\n|---|---|---|\n| `PublicKey` | `pk_` | `PublicKey::from_prefixed(s)` |\n| `SecretKey` | `sk_` | `SecretKey::from_prefixed(s)` |\n| `KeyImage` | `ki_` | `KeyImage::from_prefixed(s)` |\n| `Signature` | `blsag_` | `Signature::from_prefixed(s, ring_size)` |\n| `OwnershipProof` | `own_` | `OwnershipProof::from_prefixed(s)` |\n| `Nonce` | `nonce_` | `Nonce::from_prefixed(s)` |\n| `RingDigest` | `ring_` | `RingDigest::from_prefixed(s)` |\n\n```rust\nlet id = crypto_vote::generate_identity();\nlet pretty = id.public_key.to_prefixed();        // \"pk_…_…\"\nassert!(pretty.starts_with(\"pk_\"));\nlet back = crypto_vote::PublicKey::from_prefixed(&pretty).unwrap();\nassert_eq!(back, id.public_key);\n\n// Wrong tag is rejected even though the bytes would decode fine as hex:\nassert!(crypto_vote::KeyImage::from_prefixed(&pretty).is_err());\n```\n\n`from_prefixed` returns `Error::InvalidPrefix` (missing/wrong tag) or\n`Error::InvalidChecksum` (mistyped or corrupted value), in addition to\nthe usual length / encoding errors. The error only ever echoes one of\nthe crate's own tags back, never an arbitrary first segment, so a secret\npasted in the wrong slot cannot end up in a log line.\n\n**The WASM, Extism and CLI front ends speak the prefixed format\n*exclusively*** — it is all they emit and all they accept; bare hex is\nrejected at those boundaries. The bare-hex `to_hex`/`from_hex` pair is\nreserved for the **pure-Rust library API**, where the caller is trusted\nto know what it is decoding. This keeps every value that crosses a\nhigh-level boundary self-describing and checksum-protected.\n\n## Private key format and properties\n\nA secret key is a **uniformly random scalar in the Ristretto255 scalar\nfield** — mathematically an integer in `[1, ℓ)`, where ℓ is the prime\norder of the group (`ℓ = 2^252 + 27742317777372353535851937790883648493`,\n≈ 2²⁵². So roughly 2²⁵² distinct keys exist). The crate ships two\ninterchangeable encodings; pick the one that matches your transport:\n\n| Encoding | Shape | Where it's used |\n|---|---|---|\n| **Raw bytes** | `[u8; 32]` little-endian | `SecretKey::{to,from}_bytes`, `sign_vote_*_wasm` and `is_valid_secret_key_wasm` secret inputs |\n| **Hex** | 64 lowercase characters | `SecretKey::{to,from}_hex` — pure-Rust library API only |\n| **Prefixed** | `sk_<64 hex>_<8 hex>` | `SecretKey::{to,from}_prefixed`, Extism plugin, CLI (see [Encoding formats](#encoding-formats)) |\n\nAll three encodings carry the same value bit-for-bit. The prefixed form\nis the **only** one the Extism and CLI front ends accept (bare hex is\nrejected there — it is reserved for the pure-Rust library API); raw\nbytes is the only\nformat the wasm-bindgen flavour accepts for secret material, because a\n`Uint8Array` is mutable and the JS caller can wipe it with `.fill(0)`\n— a JS `String` (and therefore a hex string) cannot be erased from the\nheap on demand.\n\n### Validity rules\n\nEvery parser (`SecretKey::from_bytes`, `from_hex`, and the WASM /\nExtism entry points) enforces three checks. A key that fails any of\nthem is rejected, never silently coerced:\n\n1. **Length.** Exactly 32 decoded bytes — `Error::InvalidLength`.\n   Hex input must therefore be 64 characters.\n2. **Canonical encoding modulo ℓ.** The 32 bytes must decode to a\n   scalar in `[0, ℓ)` — `Error::InvalidScalar`. The library refuses\n   any \"non-reduced\" encoding (e.g. a value ≥ ℓ but < 2²⁵⁶), because\n   that would let two different byte strings designate the same key\n   — a classic malleability footgun.\n3. **Non-zero.** The zero scalar is rejected — `Error::InvalidSecretKey`.\n   Its derived public key would be the Ristretto identity point, which\n   the protocol cannot use as a participant (and which `PublicKey`\n   independently refuses with `Error::InvalidIdentityPoint`).\n\n### Cryptographic properties\n\n- **The public key is fully determined by the secret key.** Internally\n  `public_key = secret_key · G` where `G` is the Ristretto255 base\n  point. Round-tripping a secret through `to_bytes` → `from_bytes`\n  derives the **same** public key, so you only need to persist the\n  32-byte secret — the public key (and therefore the ring entry) can\n  always be re-derived. There is no separate key-schedule, clamping,\n  or hashing step applied to the secret.\n- **Same key, same ring, same election ⇒ same key image.** This is\n  what makes double-vote detection possible. The key image is\n  `I_e = x · H_p(domain ‖ election_id ‖ x · G)`, so it is fully\n  deterministic in `(secret_key, election_id)` and changes between\n  elections (see [Election binding](#election-binding)).\n- **Entropy.** `generate_identity` samples from `SysRng` —\n  `getrandom(2)` / `getentropy(2)` / `BCryptGenRandom` natively,\n  `Crypto.getRandomValues` in the browser. Any uniform value in\n  `[1, ℓ)` is a valid secret. **Never** derive a secret from a user\n  password without a strong KDF (Argon2id / scrypt) producing 32\n  uniform bytes — the protocol's anonymity rests on the secret being\n  unguessable.\n- **Memory hygiene.** Inside Rust the scalar lives in a `SecretKey`\n  whose `Drop` impl calls `Zeroize`. Every transient buffer that\n  touches the secret on the WASM / Extism boundary is wrapped in\n  `Zeroizing`. The JS-side `Uint8Array` is the caller's\n  responsibility — see [Operation A](#operation-a--generate-an-identity)\n  for the wipe pattern. The hex form gives up that last step (a JS\n  `String` is immutable), which is why the wasm-bindgen flavour keeps\n  the secret as bytes.\n\n### Validating a secret key without constructing one\n\nReading a stored secret back and want a quick \"is this still a usable\nkey?\" check before doing anything else? Two zero-cost helpers apply\nthe three rules above and return a plain `bool`:\n\n```rust\nuse crypto_vote::SecretKey;\n\nlet bytes: [u8; 32] = load_from_disk();\nif !SecretKey::is_valid_bytes(&bytes) {\n    return Err(\"stored secret is corrupted\");\n}\n\nassert!(SecretKey::is_valid_hex(\"aabbccdd…\"));        // canonical, non-zero\nassert!(!SecretKey::is_valid_hex(\"not hex\"));          // bad encoding\nassert!(!SecretKey::is_valid_hex(&\"00\".repeat(32)));   // zero scalar\n\n// Same check for the prefixed form (tag + checksum must also be valid):\nassert!(SecretKey::is_valid_prefixed(\"sk_aabbccdd…_1a2b3c4d\"));\n```\n\nThese are the same checks performed implicitly at the start of every\n`sign_vote` call, so calling them up-front is purely a convenience —\nuseful for surfacing a clear UI message before the user has filled in\nthe rest of the form. The WASM and Extism layers expose the same\nhelper (see below).\n\n## Using the command line\n\n```bash\n# Generate an identity (one per voter). Output uses the prefixed format.\n$ cryptovote keygen\nsecret=sk_…_…\npublic=pk_…_…\n\n# Sign a ballot. `ring.txt` is one prefixed public key (`pk_…`) per\n# line. Keep the secret in a file or pass `--secret -` to read it from\n# stdin.\n$ cryptovote sign --secret-file secret.key --vote \"option-A\" \\\n    --election-id \"election-2026-05\" --ring ring.txt\nsignature=blsag_…_…\nkey_image=ki_…_…\n\n# Verify. Exit code 0 = valid, 1 = invalid, 2 = bad input. The\n# --signature / --key-image values are prefixed (`blsag_…`, `ki_…`).\n$ cryptovote verify --vote \"option-A\" --election-id \"election-2026-05\" \\\n    --signature blsag_…_… --key-image ki_…_… --ring ring.txt\nvalid\n\n# Fingerprint an authorised list (order-independent), to publish next to\n# the ring or to compare against a published value.\n$ cryptovote ring-digest --ring ring.txt\ndigest=ring_…_…\n```\n\nEvery key / signature argument is the prefixed form (`pk_…`,\n`sk_…`, `ki_…`, `blsag_…`); bare hex is rejected. The CLI always prints\nthe prefixed form.\n\n## Vote payload format\n\nThe library treats the vote as **opaque bytes** — `sign_vote` and\n`verify_vote` both take `&[u8]`, with no parsing and no built-in size\nlimit (the host should enforce one; see the [threat model](#threat-model)).\nJSON, Protobuf, raw text, or arbitrary binary all work the same way.\nThe WASM and Extism bindings expose **two entry points** per operation\nto cover this: a *text* one (`vote` as a UTF-8 string) and a *binary*\none (`vote` as a `Uint8Array` in WASM, hex-encoded in Extism). Both\nfunnel into the same `&[u8]` core, so a ballot signed through one is\nverifiable through any of them as long as the bytes match. See those\nsections below.\n\n**The contract for the host:** what you sign is what you store is what\nyou verify, byte-for-byte. If the host re-encodes the payload between\nreceiving it and verifying it — JSON re-serialisation, BOM stripping,\nline-ending normalisation, Unicode normalisation, lowercasing,\nanything — verification *will* fail, because Blake2b is sensitive to\nevery single byte. The safe rule is: persist the raw bytes received\nfrom the voter, and hand those same bytes back to `verify_vote`.\n\nNote that the vote payload is treated as *raw bytes* and is never\nnormalised by the library — unlike `election_id`, which is forced to\nNFC. The asymmetry is deliberate: the election ID is a *label* the\nhost controls and re-emits across encodings, so normalising it makes\nthe API robust; the vote is *content* the host stores verbatim, so\nnormalising it would silently change what was signed.\n\n### Large or structured payloads in the browser\n\nThere are two signing entry points. Pick by ballot type:\n\n- **`sign_vote_str_wasm(secret, voteStr, electionId, ring)`** — `vote`\n  is a string. Use it for text ballots (a label, a stringified JSON\n  ballot). Most common.\n- **`sign_vote_bytes_wasm(secret, voteBytes, electionId, ring)`** —\n  `vote` is a `Uint8Array`. Use it for binary ballots (raw Protobuf,\n  any non-UTF-8 bytes).\n\nThe natural pattern for a JSON ballot is to serialise it once on the\nvoter's device and pass that exact string everywhere afterwards:\n\n```js\nconst ballot     = JSON.stringify(form);                   // a string\nconst electionId = \"550e8400-e29b-41d4-a716-446655440000\"; // plain JS string\nconst [sigHex, tagHex] = sign_vote_str_wasm(\n    secretBytes, ballot, electionId, ringHex,\n);\n// `secretBytes` is the `Uint8Array` returned by `generate_identity_wasm`\n// (or read back from your store). Wipe it with `secretBytes.fill(0)`\n// as soon as you no longer need it in memory.\n\n// Send `ballot` (the same string), `sigHex` and `tagHex` to the host.\n// The host stores `ballot` verbatim — it must never JSON.parse +\n// JSON.stringify the payload, or the re-serialised string may differ\n// byte-for-byte and verification will fail.\n\n// Binary ballot? Sign the exact bytes and store them verbatim instead:\n//   const [sigHex, tagHex] = sign_vote_bytes_wasm(\n//       secretBytes, ballotBytes /* Uint8Array */, electionId, ringHex,\n//   );\n```\n\n### Large or structured payloads on the CLI\n\nThe `--vote` flag accepts `-` to read the ballot from standard input.\nUse it for anything that exceeds your shell's argv limit, contains\nnewlines, or is binary:\n\n```bash\ncat ballot.json | cryptovote sign \\\n  --secret-file secret.hex --vote - --election-id \"election-2026-05\" --ring ring.txt\n\ncat ballot.json | cryptovote verify --vote - \\\n    --election-id \"election-2026-05\" \\\n    --signature <hex> --key-image <hex> --ring ring.txt\n```\n\n## Using from JavaScript / WebAssembly\n\nThe library exposes a thin `wasm-bindgen` layer behind the `wasm`\nfeature. The npm package and a self-built bundle are the **same**\n`--target web` artefact: a self-contained ES module that instantiates\nthe WASM itself through an async `init()` default export. It works\nthe same everywhere — a bundler (Vite, Rollup, esbuild), Bun, Node, or\na plain `<script type=\"module\">` — with **no bundler plugin required**.\nYou call `await init()` exactly once.\n\n### Install from npm\n\n```bash\nnpm install @condorcet.vote/crypto-vote\n```\n\nThe package is published to [npmjs.com](https://www.npmjs.com/package/@condorcet.vote/crypto-vote)\non every release:\n\n```js\nimport init, {\n    generate_identity_wasm,\n    derive_public_key_wasm,\n    sign_vote_str_wasm,    // text ballot\n    sign_vote_bytes_wasm,  // binary ballot (Uint8Array)\n    verify_vote_str_wasm,\n    verify_vote_bytes_wasm,\n    is_valid_secret_key_wasm,\n    secret_key_from_prefixed_wasm,      // import sk_… string → bytes\n    secret_key_to_prefixed_wasm,        // export bytes → sk_… string\n    is_valid_prefixed_secret_key_wasm,  // validate an sk_… string\n    ring_digest_wasm,                   // fingerprint a ring → ring_… string\n    ring_matches_digest_wasm,           // compare a fetched ring with a published digest\n} from \"@condorcet.vote/crypto-vote\";\n\n// Instantiate the WASM once. A bundler resolves the .wasm asset for you;\n// no plugin needed. Then every function is ready to call.\nawait init();\nconst [secretBytes, publicKey] = generate_identity_wasm();\n```\n\n> Why `--target web` and not `--target bundler`? The `bundler` target\n> relies on the non-standard WebAssembly/ESM-integration that only\n> webpack implements, so it breaks under Vite (needs `vite-plugin-wasm`)\n> and Bun (treats the `.wasm` as a static asset). `--target web` is the\n> portable choice.\n\n### Build from source\n\nSee the [Build matrix](#build-matrix) above for the full command and\nflag breakdown. The short version, run from the crate root:\n\n```bash\nwasm-pack build --profile wasm-release --target web -- --no-default-features --features wasm\n```\n\nThe resulting `pkg/` directory contains the `.wasm` artefact and the\nJS glue — the same files the npm package ships. Copy it next to your\npage or import it through your bundler.\n\n### Initialise the module\n\n`await` the default export exactly once before calling anything else;\nit streams and instantiates the `.wasm` file. This applies to both the\nnpm package and a self-built bundle:\n\n```js\nimport init, {\n    generate_identity_wasm,\n    derive_public_key_wasm,\n    sign_vote_str_wasm,    // text ballot\n    sign_vote_bytes_wasm,  // binary ballot (Uint8Array)\n    verify_vote_str_wasm,\n    verify_vote_bytes_wasm,\n    is_valid_secret_key_wasm,\n    secret_key_from_prefixed_wasm,\n    secret_key_to_prefixed_wasm,\n    is_valid_prefixed_secret_key_wasm,\n    ring_digest_wasm,\n    ring_matches_digest_wasm,\n} from \"@condorcet.vote/crypto-vote\"; // or \"./pkg/crypto_vote.js\" when self-built\n\nawait init();\n```\n\n### Operation A — generate an identity\n\n```js\n// The secret comes back as a `Uint8Array` (32 raw bytes); the public\n// key as a prefixed string (\"pk_…_…\"). The asymmetry is deliberate: a JS\n// string is immutable, so once a secret has been turned into one it\n// cannot be erased from the JS heap until the GC eventually collects it.\n// A `Uint8Array` is a mutable buffer the caller can wipe explicitly with\n// `.fill(0)` once the secret has been used or persisted.\nconst [secretBytes, publicKey] = generate_identity_wasm();\n\n// 1. Persist `secretBytes` wherever you want it to live across\n//    sessions. The library does not care which store you pick.\nawait persistVoterSecret(secretBytes);\n\n// 2. Register the public key with the host.\nawait fetch(\"/api/register\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ public_key: publicKey }),\n});\n\n// 3. Wipe the in-memory copy. After this point the only readable\n//    instance of the secret is whatever your `persistVoterSecret`\n//    produced — nothing is sitting around in the JS heap.\nsecretBytes.fill(0);\n```\n\n> **Note on the threat model.** Wiping the JS-side `Uint8Array` does\n> not protect against XSS / a malicious script running *while the\n> secret is still loaded*. What it does protect against is post-hoc\n> memory inspection: core dumps, devtools snapshots taken later, swap\n> partitions, extensions that scan page memory periodically. The\n> window of exposure is reduced to the smallest interval the caller\n> can manage. The library does its share by `Zeroizing` every\n> Rust-side copy of the secret automatically.\n\n### Validating a stored secret key\n\n`is_valid_secret_key_wasm` runs the same canonical-encoding and\nnon-zero-scalar checks the `sign_vote_*_wasm` functions apply internally, but\nwithout producing anything — useful for surfacing a clear UI error\nbefore the voter has filled in the rest of the form. The input is\nwrapped in `Zeroizing` on the Rust side; the caller still owns\nwiping its own `Uint8Array` afterwards.\n\n```js\nconst secretBytes = await loadVoterSecret();\nif (!is_valid_secret_key_wasm(secretBytes)) {\n    secretBytes.fill(0);\n    throw new Error(\"stored secret is corrupted or invalid\");\n}\n// secretBytes is safe to feed into the sign_vote_*_wasm functions.\n```\n\nReturns `false` (never throws) on any malformed input: wrong length,\nnon-canonical encoding, or the zero scalar.\n\n### Recovering a public key from a stored secret\n\n`derive_public_key_wasm` re-derives the public key from a 32-byte secret key.\nThe derivation is a pure scalar multiplication — no randomness — so the same\nsecret always yields the same public key. Useful when a caller has persisted\nthe secret key but needs to recover or re-display the corresponding public key\n(e.g. to re-register after a device migration, or to display a voter's ring\nentry in the UI without storing the public key separately).\n\nThe input is wrapped in `Zeroizing` on the Rust side; wipe the JS-side\n`Uint8Array` with `.fill(0)` once the call returns.\n\n```js\nconst secretBytes = await loadVoterSecret();\nlet publicKey;\ntry {\n    // Returns the prefixed public key (\"pk_…_…\"), or throws on malformed\n    // input (wrong length, non-canonical encoding, zero scalar).\n    publicKey = derive_public_key_wasm(secretBytes);\n} finally {\n    secretBytes.fill(0);\n}\n```\n\n### Importing / exporting a secret key in the prefixed format\n\nThe wasm-bindgen flavour handles secret material as a `Uint8Array` so the\nJS caller can wipe it with `.fill(0)`. But a voter who wants to **back up\nor re-import** their key works with the human-friendly prefixed string\n(`sk_<hex>_<checksum>`). These two functions bridge the gap:\n\n- `secret_key_from_prefixed_wasm(str)` — **import**: validates the `sk_`\n  tag, the checksum and the canonical-scalar rule, then returns the\n  32-byte `Uint8Array` the rest of the API consumes. Throws a clear error\n  (wrong prefix, mistyped checksum, non-canonical scalar) instead of\n  yielding a silently-wrong key.\n- `secret_key_to_prefixed_wasm(bytes)` — **export**: turns a raw\n  `Uint8Array` (e.g. the `secretBytes` from `generate_identity_wasm`) into\n  the `sk_…_…` string to show or download.\n- `is_valid_prefixed_secret_key_wasm(str)` — **validate only**: `true` /\n  `false` for live form feedback, never throws.\n\n```js\n// --- Import: the voter pastes their backup string into a form ---\nconst pasted = form.secretKey.value.trim(); // \"sk_…_…\"\n\nif (!is_valid_prefixed_secret_key_wasm(pasted)) {\n    throw new Error(\"That doesn't look like a valid secret key.\");\n}\n\nlet secretBytes;\ntry {\n    secretBytes = secret_key_from_prefixed_wasm(pasted); // → Uint8Array(32)\n    // …persist it, then use it to sign…\n    const [signature, keyImage] =\n        sign_vote_str_wasm(secretBytes, \"option-A\", electionId, ring);\n} finally {\n    secretBytes?.fill(0);\n}\n\n// --- Export: let the voter back up a freshly generated key ---\nconst [freshBytes, publicKey] = generate_identity_wasm();\ntry {\n    const backup = secret_key_to_prefixed_wasm(freshBytes); // \"sk_…_…\"\n    showDownload(backup); // a String can't be .fill(0)'d — show it, then drop it\n} finally {\n    freshBytes.fill(0);\n}\n```\n\nA `String` cannot be wiped from JS memory the way a `Uint8Array` can, so\nonly call the **export** function at the moment you actually display or\ndownload the backup, and let the reference go out of scope right after.\n\n### Operation B — sign a ballot\n\n```js\n// 1. Serialise the ballot ONCE into a string. This exact string is\n//    what gets signed, what you send to the host, and what the host\n//    must store verbatim. Do not re-stringify on the host —\n//    verification compares the UTF-8 bytes of the string.\nconst ballot = JSON.stringify({ choice: \"option-A\" });\n\n// 2. Election context the host has told the page about.\nconst electionId = \"550e8400-e29b-41d4-a716-446655440000\";\n\n// 3. The full authorised ring — one entry per authorised voter,\n//    including the current one. Each entry is a prefixed public key\n//    (\"pk_…_…\", what `generate_identity_wasm` returns); bare hex is not\n//    accepted at the WASM boundary. The module canonicalises the order\n//    internally, so the server can return them in any order\n//    (registration order, sorted, whatever):\n//\n//        GET /api/election/ring\n//        200 OK\n//        Content-Type: application/json\n//        [\n//          \"pk_84e5498b443e3617cfa8d54d9699922e2733105f287cf5bbbe90174544875b0e_1a2b3c4d\",\n//          \"pk_541e3d09e31ac28016ce4f652f591a4745920aff4103b4e3b272204812d4f153_5e6f7a8b\",\n//          ...\n//        ]\n//\n//    Each entry is exactly what `PublicKey::to_prefixed()` produces.\nconst ring = await fetch(\"/api/election/ring\").then(r => r.json());\n\n//    Anonymity is exactly \"which ring did I sign under\", so check the\n//    ring against the digest the election published out of band (see\n//    the threat model) and refuse to sign on a mismatch.\nif (!ring_matches_digest_wasm(ring, PUBLISHED_RING_DIGEST /* \"ring_…_…\" */)) {\n    throw new Error(\"the ring served by the host is not the published one\");\n}\n\n// 4. Read the secret bytes back from wherever you persisted them.\n//    `sign_vote_str_wasm` takes the secret as a `Uint8Array` of length\n//    32. Wrap the call in try/finally so the in-memory copy of the\n//    secret is wiped even on error.\nconst secretBytes = await loadVoterSecret();\nlet signature, keyImage;\ntry {\n    // `sign_vote_str_wasm` throws on bad inputs (empty vote, empty\n    // election ID, secret of the wrong length, signer not in the ring,\n    // …). The Rust side wraps the incoming bytes in `Zeroizing` so the\n    // WASM linear-memory copy is wiped on return. (For a binary ballot,\n    // call `sign_vote_bytes_wasm` with a `Uint8Array` instead.)\n    [signature, keyImage] = sign_vote_str_wasm(\n        secretBytes,\n        ballot,\n        electionId,\n        ring,\n    );\n} finally {\n    // 5. Wipe the JS-side copy of the secret as soon as signing is\n    //    done. The `Uint8Array` is the only place the secret lived in\n    //    the JS heap; after `.fill(0)` it is unreadable.\n    secretBytes.fill(0);\n}\n\n// 6. Send the proof + the ballot string to the host. The ballot must\n//    arrive byte-for-byte unchanged, so transmit it as-is (here a\n//    plain form field; a JSON body works too) and never re-serialise.\nconst form = new FormData();\nform.append(\"election_id\", electionId);\nform.append(\"signature\", signature);\nform.append(\"key_image\", keyImage);\nform.append(\"ballot\", ballot);\n\nawait fetch(\"/api/vote\", { method: \"POST\", body: form });\n```\n\n### Operation C — verify (server-side or in a Node host)\n\nThe same WASM module works in Node.js (or Deno / Bun) for hosts that\nprefer to keep verification inside a JS runtime instead of linking the\nRust crate natively:\n\n```js\nimport init, { verify_vote_str_wasm } from \"./pkg/crypto_vote.js\";\n\nawait init();\n\n// `ballot` is the exact string you stored verbatim when receiving the\n// vote — read it back without any re-encoding or re-serialisation.\n// (A binary ballot stored as bytes verifies with `verify_vote_bytes_wasm`.)\nconst isValid = verify_vote_str_wasm(\n    ballot,\n    electionId,\n    signature,\n    keyImage,\n    ring,\n);\n\n// `verify_vote_str_wasm` never throws and never returns anything other\n// than a boolean: any parse error / malformed input is just `false`.\nif (!isValid) {\n    return reject(\"invalid proof\");\n}\n```\n\n### Host-side checklist\n\nBefore calling `verify_vote_str_wasm` / `verify_vote_bytes_wasm`, the host should:\n\n1. Read `key_image` from the submission and look it up in its\n   per-election store. If present → reject (double vote).\n2. Otherwise call the matching `verify_vote_*_wasm`. On `true`, persist `key_image`\n   to the store *atomically with* recording the ballot, so a crash\n   between the two cannot let a voter slip through twice.\n\n## Extism plugin\n\nThe Extism flavour ships **one `.wasm` artefact that runs in every\nExtism host SDK** — browser via `@extism/extism`, Node, Deno, Bun,\nPython, Go, Rust, Java, even the standalone `extism` CLI. Same binary,\nsame calls, regardless of the host language. Build it with\n`--features extism --target wasm32-wasip1` (see the build matrix\nabove); WASI provides the RNG, no extra wiring required.\n\n### Recommended split: pick the right flavour per role\n\nThe two flavours are **not interchangeable** for secret-handling code.\nThe architectural split that matches their trade-offs:\n\n| Role | Flavour | Why |\n|---|---|---|\n| **Voter device** (browser, signs ballots) | wasm-bindgen | Secret is exposed as a mutable `Uint8Array` that the JS caller can `.fill(0)`. Every Rust-side temporary is `Zeroizing`-wrapped. This is the only flavour built around memory hygiene for the secret. |\n| **Verifier** (server, mobile app, audit tool, CLI tooling, …) | **Extism** | Verification never touches a secret — `verify_vote_str` / `verify_vote_hex` only handle public bytes (signature, key image, ring, ballot). One binary supports verifiers written in any host language. |\n\nThe Extism flavour *can* technically run the `sign_vote_*` functions\nand `generate_identity`, but doing so on a voter device degrades secret\nhygiene compared to wasm-bindgen — see the next subsection. For\nverifiers and non-secret-handling tooling, the Extism flavour is the\nright default.\n\n### Secret-hygiene trade-off vs wasm-bindgen\n\nGoing through JSON means the secret key transits as a (prefixed) string\nin the plugin's input/output buffer. Three protections that the\nwasm-bindgen flavour provides are weakened or lost:\n\n- The **Rust-side intermediate `String`** holding the parsed secret\n  *is* wrapped in `Zeroizing` (its heap allocation is overwritten when\n  the sign call returns). However the JSON parser's internal buffers and\n  the Extism PDK's input buffer in WASM linear memory are not under\n  our control.\n- The **JS-side `String`** holding the secret is **immutable** — the\n  host cannot call `.fill(0)` on it. It lives until V8 garbage-collects\n  it (no guaranteed timing).\n- The **Extism input/output buffers** in WASM linear memory may\n  persist between plugin calls (Extism re-uses the instance), so the\n  secret bytes can linger across calls until a future allocation\n  overwrites the region.\n\nNet effect: the Extism flavour reduces *post-hoc* memory hygiene\n(core dumps, devtools snapshots taken later, swap to disk, periodic\nmemory scans). It does **not** change the *live* attack surface — a\nscript running in the same context while the secret is in memory can\nread it under both flavours. If your threat model prioritises the\npost-hoc class, sign on a voter device with the wasm-bindgen flavour.\n\n### Plugin function signatures\n\nSign and verify each come in two flavours, differing only in how the\n`vote` field is carried:\n\nEvery key / signature field below uses the prefixed form (`pk_…`,\n`sk_…`, `ki_…`, `blsag_…`), both in and out — bare hex is **rejected**\nat the plugin boundary (it is only accepted by the pure-Rust library\nAPI). Below, `<pk>` etc. denote a prefixed value; `<vote>` is unaffected.\nSee [Encoding formats](#encoding-formats).\n\n| Plugin function | JSON input | JSON output |\n|---|---|---|\n| `generate_identity` | *(empty)* | `{\"secret\": <sk>, \"public\": <pk>}` |\n| `derive_public_key` | `{\"secret\": <sk>}` | `{\"public\": <pk>}` |\n| `sign_vote_str` | `{\"secret\": <sk>, \"vote\": <str>, \"election_id\": <str>, \"ring\": [<pk>, …]}` | `{\"signature\": <blsag>, \"key_image\": <ki>}` |\n| `sign_vote_hex` | `{\"secret\": <sk>, \"vote\": <hex>, \"election_id\": <str>, \"ring\": [<pk>, …]}` | `{\"signature\": <blsag>, \"key_image\": <ki>}` |\n| `verify_vote_str` | `{\"vote\": <str>, \"election_id\": <str>, \"signature\": <blsag>, \"key_image\": <ki>, \"ring\": [<pk>, …]}` | `{\"valid\": <bool>}` |\n| `verify_vote_hex` | `{\"vote\": <hex>, \"election_id\": <str>, \"signature\": <blsag>, \"key_image\": <ki>, \"ring\": [<pk>, …]}` | `{\"valid\": <bool>}` |\n| `is_valid_secret_key` | `{\"secret\": <sk>}` | `{\"valid\": <bool>}` |\n| `ring_digest` | `{\"ring\": [<pk>, …]}` | `{\"digest\": <ring>}` |\n\n- **`_str`** — `vote` is a plain JSON string; its UTF-8 bytes *are* the\n  ballot, fed verbatim with no decoding. Ergonomic for text ballots (a\n  label, a stringified JSON ballot). A JSON string only carries valid\n  UTF-8, so this flavour can't represent a non-UTF-8 ballot.\n- **`_hex`** — `vote` is hex-encoded bytes, decoded before hashing. Use\n  it for *arbitrary binary* ballots (raw Protobuf, NUL bytes, any\n  non-UTF-8 sequence). This `hex` concerns only the *ballot*; the keys,\n  signatures and tags on the same wire use the prefixed format.\n\nThe two are just front doors to the same byte-level operation — the\nlibrary always sees `&[u8]`. So a proof made with `sign_vote_str`\nverifies under `verify_vote_hex` and vice-versa, as long as the bytes\nmatch (signing `\"oui\"` with `_str` == signing `\"6f7569\"` with `_hex`).\nThis is also what lets a ballot signed by either wasm-bindgen function\nverify here: only the vote bytes matter. A binary ballot signed in the\nbrowser with `sign_vote_bytes_wasm` is verified on the server by\nhex-encoding those stored bytes and calling `verify_vote_hex`.\n\n`derive_public_key` re-derives the public key from a stored secret key —\nuseful when a caller has persisted only the secret and needs to recover the\ncorresponding ring entry. Input: `{\"secret\": <sk>}` (prefixed `sk_…`).\nOutput: `{\"public\": <pk>}` (prefixed). Errors (bad prefix, bad checksum,\nbad hex, wrong length, zero scalar) are returned as plugin-level errors.\nThe Rust-side copy of the secret is wrapped in `Zeroizing`; the same\nJSON-buffer caveat as `sign_vote_*` applies.\n\n```bash\nextism call crypto_vote-extism.wasm derive_public_key \\\n    --input '{\"secret\":\"sk_…_…\"}' --wasi\n# → {\"public\":\"pk_…_…\"}\n```\n\n`is_valid_secret_key` is a pure utility that mirrors\n`SecretKey::is_valid_prefixed`: it returns `{\"valid\": false}` (never an\nerror) for malformed input — bad prefix/checksum, bad hex, wrong length,\nnon-canonical encoding, or the zero scalar. The Rust-side copy of the secret is\nwrapped in `Zeroizing`, but the same JSON-buffer caveat as the\n[`sign_vote_*`](#secret-hygiene-trade-off-vs-wasm-bindgen) functions\napplies; for secret-handling on a voter device, prefer the wasm-bindgen\nflavour.\n\n### Browser example\n\n> The browser snippet below exercises the identity, sign and verify\n> functions for completeness, but in a real deployment a voter device should sign\n> with the [wasm-bindgen flavour](#using-from-javascript--webassembly)\n> for better secret hygiene. The Extism flavour in the browser is\n> idiomatic for *verifier* roles: audit pages, results dashboards,\n> mobile webview verifiers, etc.\n\n```js\n// One-time install:  npm install @extism/extism\nimport createPlugin from \"@extism/extism\";\n\n// `useWasi: true` is required because the plugin is built for\n// wasm32-wasip1 (so `SysRng` can read random bytes from the WASI\n// shim). The browser SDK ships a WASI polyfill internally.\nconst plugin = await createPlugin(\n    \"/static/crypto_vote-extism.wasm\",\n    { useWasi: true },\n);\n\n// --- Operation A — generate an identity ---\nconst { secret, public: publicKey } = await plugin\n    .call(\"generate_identity\", \"\")\n    .then(out => out.json());\n\n// `secret` is a prefixed string (\"sk_…_…\"). Persist it however you want;\n// this flavour does not offer the `.fill(0)` zeroisation hook that the\n// wasm-bindgen flavour does.\nawait persistVoterSecret(secret);\n\n// --- Operation B — sign a ballot ---\n// Text ballot → `sign_vote_str`, vote passed through unchanged, no hex\n// step. (Binary ballot → `sign_vote_hex` with `vote` set to the hex of\n// your bytes.)\nconst ballot = JSON.stringify({ choice: \"option-A\" });\n\nconst ring = await fetch(\"/api/election/ring\").then(r => r.json());\n\nconst { signature, key_image } = await plugin\n    .call(\"sign_vote_str\", JSON.stringify({\n        secret,\n        vote:        ballot,\n        election_id: \"election-2026\",\n        ring,\n    }))\n    .then(out => out.json());\n\n// --- Operation C — verify (also works server-side; see below) ---\n// Use the flavour matching how the ballot was signed: `verify_vote_str`\n// for a string vote, `verify_vote_hex` for a hex one.\nconst { valid } = await plugin\n    .call(\"verify_vote_str\", JSON.stringify({\n        vote:        ballot,\n        election_id: \"election-2026\",\n        signature,\n        key_image,\n        ring,\n    }))\n    .then(out => out.json());\n```\n\n### Node.js / Deno / Bun example\n\n**Same code as the browser** — that is the whole point of the Extism\nflavour. The only difference is how you load the `.wasm` (a file path\non the server, a URL in the browser):\n\n```js\nimport createPlugin from \"@extism/extism\";\nimport { readFileSync } from \"node:fs\";\n\nconst plugin = await createPlugin(\n    { wasm: [{ data: readFileSync(\"./crypto_vote-extism.wasm\") }] },\n    { useWasi: true },\n);\n\nconst { valid } = await plugin\n    .call(\"verify_vote_str\", JSON.stringify({ /* ... same shape ... */ }))\n    .then(out => out.json());\n```\n\nThe host SDK is also available for [Python](https://github.com/extism/python-sdk),\n[Go](https://github.com/extism/go-sdk),\n[Rust](https://github.com/extism/extism/tree/main/runtime),\n[Java](https://github.com/extism/java-sdk), and others — the JSON\nshapes above are identical for all of them.\n\n### Quick smoke test from the CLI\n\nThe `extism` standalone CLI is the fastest way to confirm the plugin\nloads and behaves correctly before integrating it anywhere:\n\n```bash\n# Install once: cargo install extism-cli   (or grab a release binary)\nextism call crypto_vote-extism.wasm generate_identity --wasi\n# → {\"secret\":\"…\",\"public\":\"…\"}\n```\n\n## Tests\n\n```bash\ncargo test\n```\n\nTests cover round-trips, same-election deterministic tags,\ncross-election tag separation, ring-order independence, bit-level\nmalleability resistance on both the signature and the key image, ring\ndigest stability, and every documented \"invalid\" case (tampered vote,\ntampered signature, swapped tag, wrong ring, subset/superset ring,\nmalformed inputs). `tests/upgrade_vectors.rs` freezes a signature, a key\nimage, a ring digest and every prefixed encoding so a protocol drift is\ncaught before release.\n\n### Linting\n\nThree commands mirror the CI lint job exactly — run them before pushing:\n\n```bash\n# 1. Formatting — must produce no diff.\ncargo fmt --all -- --check\n\n# 2. Clippy — default features (CLI + native library).\ncargo clippy --all-targets -- -D warnings\n\n# 3. Clippy — WASM feature, cross-compiled to wasm32 so wasm-bindgen\n#    macros type-check correctly. Requires the target to be installed:\n#    rustup target add wasm32-unknown-unknown\ncargo clippy --no-default-features --features wasm \\\n    --target wasm32-unknown-unknown --lib -- -D warnings\n```\n\nTo auto-fix formatting instead of just checking: `cargo fmt --all`.\n\n### Fuzzing\n\nA `cargo-fuzz` harness lives in [`fuzz/`](fuzz/) with four targets:\n\n```bash\ncargo install cargo-fuzz   # one-off\ncargo +nightly fuzz run verify_vote      # parse + verify pipeline\ncargo +nightly fuzz run parse_signature  # Signature::from_bytes\ncargo +nightly fuzz run parse_keys       # PublicKey / SecretKey / KeyImage\ncargo +nightly fuzz run roundtrip        # sign → verify differential\n```\n\nThe `fuzz/` package is excluded from the main workspace so it does\nnot affect stable builds.\n\n## Layout\n\n```\nsrc/\n├── lib.rs            — crate entry point, re-exports, top-level docs\n├── error.rs          — `Error` enum (input parsing only)\n├── types.rs          — PublicKey / SecretKey / Signature / KeyImage / VoteProof\n├── identity.rs       — Operation A\n├── blsag.rs          — BLSAG with election-scoped key images and hedged nonces\n├── signing.rs        — Operation B (ring canonicalisation + NFC of election_id)\n├── verifying.rs      — Operation C\n├── wasm.rs           — wasm-bindgen layer (feature-gated, Zeroizing secrets)\n├── extism.rs         — Extism PDK layer (feature-gated, JSON-over-WASI)\n└── main.rs           — CLI binary\ntests/\n├── integration.rs    — public-API round-trips, malleability, negative paths\n└── upgrade_vectors.rs — frozen test vectors (protocol-drift tripwires)\nfuzz/\n├── Cargo.toml        — separate package, excluded from the workspace\n└── fuzz_targets/     — four cargo-fuzz harnesses (see \"Fuzzing\")\nCHANGELOG.md          — release notes, one entry per published version\n```\n\n## License\n\nThis project is licensed under the **GNU Affero General Public License\nv3.0 or later** (AGPL-3.0-or-later). See [LICENSE](LICENSE) for the\nfull text.\n\nThe AGPL is a strong copyleft licence. In short: anyone running a\nmodified version of this code as a network service must make their\nmodifications available to its users. If that obligation is\nincompatible with your use case, please open an issue before\nintegrating the crate.\n","readmeFilename":"README.md"}