Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
- run: npm ci --ignore-scripts
- run: npm run check:types
- run: npm run test:installer
- name: BIP-322 exact-overlay crypto vectors
run: npm run test:bip322:crypto
- name: Four-package archive inventory, immutable iOS and refusal controls
run: python3 -B scripts/test-distribution-archives.py
- name: Build pinned graph and verify provenance
Expand Down
32 changes: 32 additions & 0 deletions BIP322.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Bitcoin message proofs (BIP-322 simple)

The maintained Bare and Node binding overlay adds address-based Bitcoin message proofs without changing the upstream RLN repository, its Lightning message signer, or the remote signer protocol. It does not enable Lightning in an application.

```js
const address = node.getMessageSigningAddress()
const signature = node.signMessageBip322(' exact message\n', address)
const valid = verifyMessageBip322(' exact message\n', address, signature, 'regtest')
```

`getMessageSigningAddress()` and `signMessageBip322()` require an unlocked node with its matching native external signer attached. The canonical signing address is the first vanilla Taproot address at `m/86'/coin_type'/0'/0/0` (coin type 0 on mainnet, 1 on other networks). It is stable, can differ from the current rotating receive address, and does not allocate an address or update wallet state. This initial version deliberately signs only that address. Supplying another address, including another address from the same wallet, fails.

The private key derives inside the native signer from the same 32-byte seed and account derivation as the pinned VLS adapter. Its account xpub is checked against the actual native signer bootstrap before admission. Only the first address scalar is retained, under the signer's existing read/write shutdown gate. Shutdown waits for active signing and destroys that scalar with the backend state, revoking all retained signer clones. Before every proof/address request, the attachment must match both the live unlocked node identity and every persisted wallet bootstrap field. This rejects replacing the attached signer after a wallet is already unlocked. Existing native node locking and terminal shutdown checks apply. No private key is returned to JavaScript or written to a new file.

Messages are exact valid UTF-8: no trimming, normalization, prefix length, or terminating NUL is added. Empty messages and embedded NUL are supported. The maximum is 4,096 UTF-8 bytes; malformed UTF-16 input is rejected before marshalling. New methods use escaped JSON across the C boundary so NUL cannot truncate a message. No caller-supplied transaction, PSBT, digest, derivation path or sighash flag is accepted. The only transactions constructed are BIP-322's unspendable virtual transactions; there is no network request or broadcast path.

The signer emits a `smp`-prefixed simple proof, as required by the finalized BIP-322 v2.0.0 format. Taproot key-path signatures use `SIGHASH_DEFAULT` and fresh OS auxiliary randomness. Legacy `signMessage()` and `verifyMessage()` retain their Lightning node-identity semantics; they are not address proofs.

The module-level `verifyMessageBip322(message, address, signature, network)` needs no node, wallet, signer, unlocked session or network connection. The node method `verifyMessageBip322(message, address, signature)` uses the configured node network and can verify while locked. Verification accepts finalized `smp` and historical prefixless simple encodings for:

- P2TR key-path: one 64-byte DEFAULT or 65-byte ALL signature; no annex or script path.
- P2WPKH: compressed public key, exact key hash, strict DER, low-S and ALL only.

The verifier rejects malformed/noncanonical base64, nonminimal or oversized witness fields, trailing bytes, extra items and other sighash types. Unsupported address/script forms, full and proof-of-funds formats are not supported. Unsupported address types are rejected with an error or `false`; the API never reports them as successfully verified. A cryptographic failure returns `false`. Messages, addresses, signatures and network parameters have explicit native and JS bounds. A proof does not prove current funds, establish the signer's real-world identity, or grant a transaction authorization. Applications must present the exact address/message and apply their own fresh authorization and request-domain policy.

The runtime capability is `bip322-simple-v1`. Consumers must check both this compiled capability and the corresponding method exports; old native binaries must fail admission rather than silently fallback to Lightning signing.

## Verification

Run `npm run test:bip322:crypto` with Rust 1.94.0. The harness extracts the exact crypto module from the pinned native overlay, then checks its virtual transaction hashes, supported official positive/negative vectors, exact UTF-8/limits/ownership, malformed witness boundaries, and independent scure/noble P2TR and P2WPKH vectors. Test keys are published public fixtures and must never hold funds. `npm run test:installer`, `npm run check:types` and the host runtime tests cover facade and artifact/lifecycle admission. Native target builds require the normal documented private upstream dependencies; this feature introduces no new native dependency or floating source pin.

Sources: [BIP-322 v2.0.0](https://github.com/bitcoin/bips/blob/4061a54418f62a5a1ae44f4604e56373329bfad3/bip-0322.mediawiki), [official basic vectors](https://github.com/bitcoin/bips/blob/4061a54418f62a5a1ae44f4604e56373329bfad3/bip-0322/basic-test-vectors.json), [official generated vectors](https://github.com/bitcoin/bips/blob/4061a54418f62a5a1ae44f4604e56373329bfad3/bip-0322/generated-test-vectors.json). Official vectors are CC0. Independent supplemental vectors were generated using public official test WIFs with @scure/btc-signer and @noble/curves; they include mathematically valid forbidden-sighash and high-S cases.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ signer/session lifecycle repair. See [NATIVE-UPGRADE-20261015.md](./NATIVE-UPGRA
for current qualification and [RELEASE-0.15-TRACKER.md](./RELEASE-0.15-TRACKER.md)
for historical restrictions.

## VSS availability

VSS is disabled by the maintained native safety policy. Supplying any `vss_url`
(including an empty string) rejects node creation before storage or network access.
Omit it or pass `null` to use the normal local wallet. Backup and fence-clear
methods remain exported for compatibility and always reject with
`ERR_VSS_UNSAFE_UPSTREAM`. See [VSS-SAFETY.md](./VSS-SAFETY.md) for scope and the
compiled capability contract; configuring a server does not enable protection.

## Installation Contract

The published wrapper contains the Darwin addon and requires three exact-version
Expand Down Expand Up @@ -215,3 +224,7 @@ and independently verify both new Android data packages, then publish the wrappe
Each publication needs its own source and builder provenance verification; a
packaging commit does not claim to rebuild the native files. These preparation
commands do not authorize publication by themselves.

## Bitcoin message signing

Use `getMessageSigningAddress()`, `signMessageBip322(message, address)` and the offline `verifyMessageBip322(message, address, signature, network)` for BIP-322 simple address proofs. The first version signs the canonical first vanilla Taproot address with the native external signer; it preserves exact UTF-8 and emits a `smp` prefix. It does not replace the existing Lightning node-identity signing API. See [BIP322.md](./BIP322.md) for supported forms, limits, custody and verification.
52 changes: 52 additions & 0 deletions VSS-SAFETY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# VSS safety policy

This maintained binding disables VSS until upstream backup security and recovery
defects are fixed. This is containment, not a replacement backup implementation.
It applies to every signer mode and network exposed by this binding.

```js
const info = getRuntimeInfo()
// info.capabilities includes 'vss-safety-policy-v1'
// info.vss_policy is frozen and exactly:
// {
// supported: false,
// code: 'ERR_VSS_UNSAFE_UPSTREAM',
// reason: 'VSS is unavailable until upstream backup security and recovery defects are fixed.'
// }
```

Any non-null `vss_url` rejects `SdkNode.create()` and compatibility
`sdkInitialize()` before a native handle, runtime, storage or remote transport is
created. Empty, whitespace and malformed URLs are also rejected. The supplied URL
and credentials are not included in the bounded denial message. Omit this field
or pass `null` for normal local-only wallet operation; this does not claim cloud
protection or change existing wallet data.

`vssBackup()` and `vssClearFence()` remain exported but are unavailable. Their
native path returns `Rln(FailedVssInit): ERR_VSS_UNSAFE_UPSTREAM: ...` without
accessing a remote server or clearing a fence. JavaScript facade bypasses, direct
Bare/N-API entry points and shared C-FFI initialization follow the same native
policy. Raw malformed arguments can still return the existing input-validation
error; they never enable transport. There is no bypass flag, automatic takeover,
new scheduler or new credential/backup protocol.

The read-only policy metadata describes runtime availability. It is not a remote
health check, proof that an existing archive is current, or verification of a
restored wallet. Existing archives are not read, modified or deleted by this
policy. It does not make already uploaded backups safe.

The pinned upstream external-signer VSS identity derives authentication and
encryption credentials from public bootstrap metadata. Additional upstream gaps
include non-mainnet namespace collisions, incomplete automatic mutation hooks,
chunked replacement atomicity and restore/fence recovery. These require a reviewed
upstream security/recovery contract and fresh qualification before enabling VSS;
passing a reachable URL or successful small backup is insufficient. Source pins
and upstream repositories are unchanged.

Native C-FFI tests check all supported networks, raw instance/global entry points,
empty/malformed URLs, an unchanged existing directory and a nonexistent directory,
and zero accepted connections at a loopback trap. The normal host canary also
checks the compiled policy, direct native bypass refusal, omitted/null local
operation and backup/fence refusal. Existing local signer reopen and BIP-322
qualification remain separate regressions. Run the normal package tests after
building matching native artifacts; old binaries fail provenance admission.
13 changes: 13 additions & 0 deletions binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,15 @@ static js_value_t *fn_estimate_fee(js_env_t *env, js_callback_info_t *info) {

FN_NODE_JSON(send_onion_message, rln_send_onion_message)
FN_NODE_STR(sign_message, rln_sign_message)
FN_NODE(get_message_signing_address, rln_get_message_signing_address)
FN_NODE_JSON(sign_message_bip322, rln_sign_message_bip322)
FN_NODE_JSON(verify_message_bip322, rln_verify_message_bip322)
static js_value_t *fn_verify_message_bip322_offline(js_env_t *env, js_callback_info_t *info) {
js_value_t *args[1]; get_args(env, info, args, 1);
CStringArg request_json(env, args[0]);
if (!request_json.valid) return make_undefined(env);
return handle_result_string(env, rln_verify_message_bip322_offline(request_json));
}

static js_value_t *fn_verify_message(js_env_t *env, js_callback_info_t *info) {
js_value_t *args[3];
Expand Down Expand Up @@ -1119,6 +1128,10 @@ rgb_lightning_node_bare_exports(js_env_t *env, js_value_t *exports) {
// Onion / signing / diagnostics
EXPORT("sendOnionMessage", send_onion_message);
EXPORT("signMessage", sign_message);
EXPORT("getMessageSigningAddress", get_message_signing_address);
EXPORT("signMessageBip322", sign_message_bip322);
EXPORT("verifyMessageBip322", verify_message_bip322);
EXPORT("verifyMessageBip322Offline", verify_message_bip322_offline);
EXPORT("verifyMessage", verify_message);
EXPORT("checkIndexerUrl", check_indexer_url);
EXPORT("checkProxyEndpoint", check_proxy_endpoint);
Expand Down
20 changes: 18 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,10 @@ export class SdkNode {
shutdown(): void

// VSS / APay
vssClearFence(request: JsonRequest): void
vssBackup(): JsonObject
/** Unavailable under the compiled VSS safety policy; no remote request is made. */
vssClearFence(request: JsonRequest): never
/** Unavailable under the compiled VSS safety policy; no remote request is made. */
vssBackup(): never
/** @deprecated Unsupported by released RLN; always throws UnsupportedCapabilityError. */
vssDeleteAll(request: { password: string }): never
apayNew(hostNodeId: string): JsonObject
Expand Down Expand Up @@ -325,6 +327,12 @@ export class SdkNode {
postAssetMedia(request: JsonRequest): JsonValue

// Signing / onion / diagnostics
/** Canonical first vanilla BIP86 receive address, with no allocation/rotation. Requires unlocked native signer. */
getMessageSigningAddress(): string
/** Exact UTF-8 message (0..4096 bytes); requested address must match getMessageSigningAddress(). Returns smp-prefixed proof. */
signMessageBip322(message: string, address: string): string
/** Read-only; P2TR key-path and compressed-key P2WPKH simple proofs only. */
verifyMessageBip322(message: string, address: string, signature: string): boolean
signMessage(message: string): JsonObject
verifyMessage(message: string, signature: string): { valid: boolean }
sendOnionMessage(request: JsonRequest): JsonValue
Expand All @@ -343,6 +351,11 @@ export interface RuntimeInfo {
readonly lock_sha256: string
readonly target: string
readonly capabilities: readonly string[]
readonly vss_policy: Readonly<{
supported: false
code: 'ERR_VSS_UNSAFE_UPSTREAM'
reason: 'VSS is unavailable until upstream backup security and recovery defects are fixed.'
}>
}
export interface RefreshTransfersResponse {
transfers: Record<string, {
Expand All @@ -358,3 +371,6 @@ export function getRuntimeInfo(): RuntimeInfo
export function uniffiIsInitialized(): boolean
export function sdkInitialize(request?: JsonRequest): void
export function sdkShutdown(): void

/** Pure offline verification; accepts smp and historical unprefixed simple proofs, never full/PoF. */
export function verifyMessageBip322(message: string, address: string, signature: string, network: string): boolean
35 changes: 14 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
*/

const binding = require('./binding')
const { parse, stringify, paymentRequest, burnRequest, unsupported, UnsupportedCapabilityError } = require('./json-boundary')
const { parse, stringify, bip322Request, paymentRequest, burnRequest, unsupported, UnsupportedCapabilityError } = require('./json-boundary')
const expectedRuntime = require('./runtime-contract.json')
const runtimeInfo = parse(binding.getRuntimeInfo())
for (const [key, expected] of Object.entries(expectedRuntime)) {
if (runtimeInfo[key] !== expected) throw new Error(`Native artifact identity mismatch: ${key}; rebuild the package`)
}
Object.freeze(runtimeInfo.capabilities)
if (runtimeInfo.vss_policy) Object.freeze(runtimeInfo.vss_policy)
Object.freeze(runtimeInfo)
exports.getRuntimeInfo = () => runtimeInfo
exports.UnsupportedCapabilityError = UnsupportedCapabilityError
Expand Down Expand Up @@ -85,30 +86,12 @@ class SdkNode {
this._closed = true
}

/**
* Forces takeover of a stale VSS ownership fence after the previous
* node died holding it. Throws `Rln(FailedVssInit)` if VSS isn't
* configured. Pointing two live nodes at the same VSS store corrupts
* state — call only when certain the previous owner is gone.
*
* @param {{ password: string }} request
*/
/** Unavailable: native policy throws ERR_VSS_UNSAFE_UPSTREAM without remote access. */
vssClearFence (request) {
binding.sdkNodeVssClearFence(this._handle, stringify(request))
}

/**
* Force an immediate VSS backup flush. Returns `{ version }` where
* version is the snapshot index just persisted. Throws if VSS isn't
* configured (no `vssUrl` at init) or the flush fails (server
* unreachable, auth rejected). Useful for app-controlled
* checkpoints (e.g. "save state before app suspend") rather than
* relying on the implicit on-write flush.
*
* Backed by the released `vss_backup()` C-FFI method.
*
* @returns {{version: number}}
*/
/** Unavailable: native policy throws ERR_VSS_UNSAFE_UPSTREAM without remote access. */
vssBackup () {
return parse(binding.sdkNodeVssBackup(this._handle))
}
Expand Down Expand Up @@ -455,6 +438,11 @@ class SdkNode {
sendOnionMessage (request) {
return parse(binding.sendOnionMessage(this._handle, stringify(request)))
}
/** Canonical first vanilla BIP86 address; never allocates or rotates an address. */
getMessageSigningAddress () { return parse(binding.getMessageSigningAddress(this._handle)) }
/** BIP-322 simple P2TR key-path proof, prefixed smp; exact UTF-8, at most 4096 bytes. */
signMessageBip322 (message, address) { return parse(binding.signMessageBip322(this._handle, bip322Request({ message, address }))) }
verifyMessageBip322 (message, address, signature) { return parse(binding.verifyMessageBip322(this._handle, bip322Request({ message, address, signature }))) }
signMessage (message) {
return parse(binding.signMessage(this._handle, message))
}
Expand Down Expand Up @@ -579,3 +567,8 @@ class NativeExternalSigner {
}

exports.NativeExternalSigner = NativeExternalSigner

/** Pure offline P2TR key-path/P2WPKH simple verification; no wallet or network requests. */
exports.verifyMessageBip322 = function (message, address, signature, network) {
return parse(binding.verifyMessageBip322Offline(bip322Request({ message, address, signature, network })))
}
22 changes: 21 additions & 1 deletion json-boundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,24 @@ function burnRequest (request) {
})
}

module.exports = { parse, stringify, paymentRequest, burnRequest, unsupported, UnsupportedCapabilityError }
function bip322Request (request) {
for (const [key, value] of Object.entries(request)) {
if (typeof value !== 'string') throw new TypeError(`BIP-322 ${key} must be a string`)
const maximum = key === 'message' || key === 'signature' ? 4096 : key === 'address' ? 128 : 16
let bytes = 0
for (let i = 0; i < value.length; i++) {
const unit = value.charCodeAt(i)
if (unit >= 0xd800 && unit <= 0xdbff) {
const next = value.charCodeAt(++i)
if (!(next >= 0xdc00 && next <= 0xdfff)) throw new TypeError('BIP-322 input contains an unpaired UTF-16 surrogate')
bytes += 4
} else if (unit >= 0xdc00 && unit <= 0xdfff) {
throw new TypeError('BIP-322 input contains an unpaired UTF-16 surrogate')
} else bytes += unit < 0x80 ? 1 : unit < 0x800 ? 2 : 3
if (bytes > maximum) throw new RangeError(`BIP-322 ${key} exceeds its byte limit`)
}
}
return stringify(request)
}

module.exports = { bip322Request, parse, stringify, paymentRequest, burnRequest, unsupported, UnsupportedCapabilityError }
2 changes: 1 addition & 1 deletion native/android-arm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"commit": "ecc69f20308231400561f6f060fea88acfa40cc4",
"lightningCommit": "79b9eb5166546c62692d3a672986f385c7fe047d",
"patch": "patches/release-adapter-v0.17.0-beta.3.patch",
"patchSha256": "7f89dc518f8fe69dc6baf62c4ee96e9d5a25d6b6ae70ab529c7b4bcc65979c90",
"patchSha256": "d8b5be9809b12acd48fa5958014b4655ff94598c99800a2fafb9e3e81809aebf",
"cffiLockSha256": "5fcdc9ad9179d3230e48973cdfa514a863d13a837752027febf8ea04713bbeb5",
"rustToolchain": "1.94.0",
"iosDeploymentTarget": "16.0",
Expand Down
4 changes: 2 additions & 2 deletions native/android-arm/runtime-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"rln_version": "0.17.0-beta.3",
"rln_commit": "ecc69f20308231400561f6f060fea88acfa40cc4",
"lightning_commit": "79b9eb5166546c62692d3a672986f385c7fe047d",
"adapter_sha256": "7f89dc518f8fe69dc6baf62c4ee96e9d5a25d6b6ae70ab529c7b4bcc65979c90",
"wrapper_sha256": "ca3ac291fbb4af86fc0d091805f9f544031298f6ce94a66b99f8f1acd763d4f7",
"adapter_sha256": "d8b5be9809b12acd48fa5958014b4655ff94598c99800a2fafb9e3e81809aebf",
"wrapper_sha256": "6c1eda8971508030a318d325517050cab3cf2c61c6ee380b6dd2d2585c4d50f5",
"lock_sha256": "5fcdc9ad9179d3230e48973cdfa514a863d13a837752027febf8ea04713bbeb5"
}
Loading
Loading