@@ -16,6 +16,7 @@ import { decode as nip19decode } from 'nostr-tools/nip19'
1616import { SimplePool } from 'nostr-tools/pool'
1717import WebSocket from 'ws'
1818import { fromNsec } from 'nsec-tree/core'
19+ import { lsagSign } from '@forgesworn/ring-sig'
1920import { fromMnemonic } from 'nsec-tree/mnemonic'
2021import { derivePersona } from 'nsec-tree/persona'
2122import { 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 }
0 commit comments