Skip to content

carsonp6/spark-java-js-signer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Spark External Signer Demo

Reference implementation showing how a Kotlin app using the Breez SDK can delegate all signing operations to a remote TypeScript server running the Spark JS SDK.

The Kotlin side never holds private keys. The TS server is the key custodian.

Architecture

┌────────────────────────┐       HTTP/JSON        ┌────────────────────────┐
│  Kotlin Client         │  ──────────────────>   │  TS Enclave Server     │
│  (Breez SDK)           │  <──────────────────   │  (Spark JS SDK)        │
│                        │                        │                        │
│  EnclaveSignerClient   │   base64-encoded       │  UnsafeStateless       │
│  implements            │   byte arrays          │  SparkSigner           │
│  ExternalSigner        │                        │                        │
└────────────────────────┘                        └────────────────────────┘

How It Works

The Breez SDK's ExternalSigner interface has ~20 methods covering ECDSA, Schnorr, FROST signing, ECIES encryption/decryption, key derivation, and secret management. EnclaveSignerClient implements every method by serializing inputs as base64 JSON, POSTing to the TS server, and deserializing the response.

FROST nonce round-tripping. The Spark JS SDK's getRandomSigningCommitment returns both the commitment (public, sent to counterparty) and the nonce (private, needed later to produce the signature share). Since the server is stateless, it returns the nonce alongside the commitment. EnclaveSignerClient.generateRandomSigningCommitment encodes the nonce as hiding (32 bytes) || binding (32 bytes) into ExternalFrostCommitments.noncesCiphertext. When signFrost is called later, it extracts those nonces from noncesCiphertext and forwards them to the server's /sign-frost endpoint.

Stateless server design. UnsafeStatelessSparkSigner holds no per-request state between calls. All signing context (including nonces) travels with the request. In a production deployment the nonces would be encrypted with a server-side key (ECIES) before leaving the server, so noncesCiphertext would be an actual ciphertext the client cannot read or tamper with.

Prerequisites

  • Java 17+
  • Gradle (wrapper included)
  • Node.js 20+, npm
  • A BIP39 mnemonic (12 or 24 words) for the signer

Quick Start

1. Start the TS signer server

cd js
npm install
SIGNER_MNEMONIC="word1 word2 ... word12" npm run dev

The server starts on port 3000 by default. Set PORT to override.

2. Run a flow (Kotlin side)

cd java

Each flow is a standalone script. Use -PmainClass=... to select:

# View balance (default)
./gradlew run

# Create a Bolt11 invoice for receiving
AMOUNT_SATS=1000 ./gradlew run -PmainClass=com.example.signer.CreateInvoiceKt

# Pay a Bolt11 invoice
BOLT11=lnbc... ./gradlew run -PmainClass=com.example.signer.PayInvoiceKt

# Claim incoming transfers (syncs and shows recent payments)
./gradlew run -PmainClass=com.example.signer.ClaimTransferKt

Set ENCLAVE_URL to override the default http://localhost:3000.

Project Structure

java-js-signer/
├── js/
│   ├── package.json          # @buildonspark/spark-sdk dependency
│   ├── tsconfig.json
│   └── src/
│       ├── server.ts         # Express server — all ExternalSigner endpoints
│       └── signer-service.ts # Thin wrapper around UnsafeStatelessSparkSigner
└── java/
    ├── build.gradle.kts      # Breez SDK 0.12.1, OkHttp 4, kotlinx-serialization
    ├── settings.gradle.kts
    └── src/main/kotlin/com/example/signer/
        ├── EnclaveSignerClient.kt  # ExternalSigner → HTTP proxy (key file)
        ├── HttpClient.kt           # OkHttp wrapper with base64 helpers
        ├── SdkSetup.kt             # Shared SDK connection logic
        ├── ViewBalance.kt          # Show wallet balance (default)
        ├── CreateInvoice.kt        # Create a Bolt11 invoice
        ├── PayInvoice.kt           # Pay a Bolt11 invoice
        └── ClaimTransfer.kt        # Sync and claim pending transfers

Endpoint Mapping

Every ExternalSigner method maps to a POST endpoint. All byte fields in request and response bodies are base64-encoded strings.

ExternalSigner method Endpoint Request fields Response fields
identityPublicKey /identity-public-key (none) bytes
derivePublicKey /derive-public-key path bytes
getPublicKeyForNode /get-public-key-for-node id bytes
signEcdsa /sign-ecdsa message, path bytes
signEcdsaRecoverable /sign-ecdsa-recoverable message, path bytes
signHashSchnorr /sign-hash-schnorr hash, path bytes
decryptEcies /decrypt-ecies ciphertext, path bytes
encryptEcies /encrypt-ecies message, path bytes
hmacSha256 /hmac-sha256 message, path bytes
generateRandomSigningCommitment /generate-random-signing-commitment (none) commitment{hiding,binding}, nonce{hiding,binding}
signFrost /sign-frost message, keyDerivation, publicKey, verifyingKey, selfCommitment, statechainCommitments?, adaptorPubKey? bytes
aggregateFrost /aggregate-frost message, publicKey, verifyingKey, selfCommitment, statechainCommitments?, selfSignature, statechainSignatures?, statechainPublicKeys?, adaptorPubKey? bytes
staticDepositSecret /static-deposit-secret index bytes
staticDepositSigningKey /static-deposit-signing-key index bytes
staticDepositSecretEncrypted /static-deposit-secret-encrypted index (secretSource object)
publicKeyFromSecret /public-key-from-secret source (keyDerivation) bytes
generateRandomSecret /generate-random-secret (none) bytes
subtractSecrets /subtract-secrets signingKey, newSigningKey (secretSource object)
splitSecretWithProofs /split-secret-with-proofs secret, threshold, numShares shares[]
encryptSecretForReceiver /encrypt-secret-for-receiver ciphertext, receiverPublicKey bytes

Production Considerations

This demo runs over plain HTTP with no authentication. Before deploying in production:

  • TLS / mTLS — Terminate TLS at the server. Use mutual TLS so only known Kotlin clients can connect.
  • Authentication — Add an Authorization header (e.g., HMAC-signed request timestamps) to prevent replay attacks.
  • FROST nonce encryption — Replace the plain hiding||binding encoding in noncesCiphertext with ECIES encryption under a server-held key. The client should treat noncesCiphertext as an opaque blob.
  • Enclave attestation — Run the TS server inside a TEE (AWS Nitro Enclaves, Azure Confidential Containers, etc.) and verify the attestation document before trusting the server.
  • KMS-backed key storage — Replace the in-memory mnemonic with a KMS-sealed seed that is unsealed only inside the enclave.
  • Rate limiting — Limit signing requests per client identity to contain blast radius if a client is compromised.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors