Skip to content

Commit 2386877

Browse files
author
ForgeSworn
committed
feat: add heartwood_lsag_sign to NIP-46 bunker
LSAG ring signature computation on the signing device. The private key never leaves the bunker. Accepts ring pubkeys, message, signerIndex, and electionId via NIP-46. Returns the LSAG signature and key image. Adds @forgesworn/ring-sig as a bunker dependency.
1 parent 481ca64 commit 2386877

3 files changed

Lines changed: 62 additions & 0 deletions

File tree

bunker/index.mjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { decode as nip19decode } from 'nostr-tools/nip19'
1616
import { SimplePool } from 'nostr-tools/pool'
1717
import WebSocket from 'ws'
1818
import { fromNsec } from 'nsec-tree/core'
19+
import { lsagSign } from '@forgesworn/ring-sig'
1920
import { fromMnemonic } from 'nsec-tree/mnemonic'
2021
import { derivePersona } from 'nsec-tree/persona'
2122
import { bytesToHex } from 'nostr-tools/utils'
@@ -661,6 +662,51 @@ async function handleRequest(event) {
661662
break
662663
}
663664

665+
case 'heartwood_lsag_sign': {
666+
// LSAG ring signature — the private key never leaves the device.
667+
// params: [JSON string of { ring: string[], message: string, signerIndex: number }]
668+
if (!Array.isArray(request.params) || typeof request.params[0] !== 'string') {
669+
error = 'heartwood_lsag_sign: missing params JSON'
670+
break
671+
}
672+
let lsagParams
673+
try {
674+
lsagParams = JSON.parse(request.params[0])
675+
} catch {
676+
error = 'heartwood_lsag_sign: invalid JSON'
677+
break
678+
}
679+
const { ring, message, signerIndex, electionId, domain } = lsagParams
680+
if (!Array.isArray(ring) || typeof message !== 'string' || typeof signerIndex !== 'number' || typeof electionId !== 'string') {
681+
error = 'heartwood_lsag_sign: requires { ring: string[], message: string, signerIndex: number, electionId: string, domain?: string }'
682+
break
683+
}
684+
if (ring.length < 2 || ring.length > 64) {
685+
error = 'heartwood_lsag_sign: ring must have 2-64 members'
686+
break
687+
}
688+
if (signerIndex < 0 || signerIndex >= ring.length) {
689+
error = 'heartwood_lsag_sign: signerIndex out of range'
690+
break
691+
}
692+
const lsagActive = getActiveSigningKey()
693+
try {
694+
const skHex = typeof lsagActive.sk === 'string' ? lsagActive.sk : bytesToHex(lsagActive.sk)
695+
const sig = domain
696+
? lsagSign(message, ring, signerIndex, skHex, electionId, domain)
697+
: lsagSign(message, ring, signerIndex, skHex, electionId)
698+
result = JSON.stringify({
699+
signature: sig,
700+
keyImage: sig.keyImage,
701+
publicKey: lsagActive.pk,
702+
})
703+
console.log(`LSAG signed: ring=${ring.length}, index=${signerIndex}, pubkey=${lsagActive.pk.slice(0, 12)}...`)
704+
} finally {
705+
releaseKey(lsagActive)
706+
}
707+
break
708+
}
709+
664710
default:
665711
error = `unsupported method: ${request.method}`
666712
}

bunker/package-lock.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bunker/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"test": "node --test 'test/**/*.test.mjs'"
99
},
1010
"dependencies": {
11+
"@forgesworn/ring-sig": "^3.0.0",
1112
"nostr-tools": "^2.23.0",
1213
"nsec-tree": "^1.4.2",
1314
"ws": "^8.20.0"

0 commit comments

Comments
 (0)