BRC-206 Wallet

Wallet-Native Rabin Signatures as a BRC-98 Module

This specification defines rabin , a BRC-98 permission-module scheme that lets an application obtain Rabin signatures over arbitrary data, and the matching Rabin public key, from a BRC-100 wallet. The Rabin private key is derived deterministically from the wallet's BRC-42/43 root for a protocol-scoped identity and never leaves the wallet. The operation is ca

Connor Murray (connor.murray@bsvassociation.org)

How it connects

Relationships

Authoritative content mirror

Full upstream text

View source ↗
Rendered from wallet/0206.md at the indexed upstream commit

Abstract

This specification defines rabin, a BRC-98 permission-module scheme that lets an application obtain Rabin signatures over arbitrary data, and the matching Rabin public key, from a BRC-100 wallet. The Rabin private key is derived deterministically from the wallet's BRC-42/43 root for a protocol-scoped identity and never leaves the wallet.

The operation is carried over the existing BRC-100 encrypt method. Inside the reserved p rabin namespace, a supporting wallet interprets encrypt semantically as "produce a Rabin public key" or "produce a Rabin signature over the plaintext" and returns the result in the ordinary { ciphertext } byte-array result. No method, call code, Wallet Wire message, or optional member is added to BRC-100.

Rabin signatures are the only digital signature that Bitcoin Script can verify over arbitrary data with a handful of opcodes (OP_MUL, OP_ADD, OP_MOD, OP_SHA256). The scheme therefore lets on-chain contracts, for example oracle-fed contracts and turn-based games, accept wallet-held attestations without any application ever holding a signing key. Wallets that do not install the module retain BRC-98's required behavior and reject the reserved protocol.

Motivation

Bitcoin Script has no OP_CHECKDATASIG. OP_CHECKSIG verifies only ECDSA signatures over the spending transaction, so a contract that must check a signature over application data (a price, a game position, an attestation) has two options: implement ECDSA in Script, which costs on the order of a megabyte of locking script per verification, or use Rabin signatures, whose verification is a fixed sequence of about twenty opcodes:

(sig² + padding) mod n == H(message)

Rabin signatures are a standard building block in compilers that target Bitcoin Script. The Rúnar compiler ships verifyRabinSig, and sCrypt contracts have used the same construction for years.

What is missing is a signer. Today the Rabin private key, the pair of primes (p, q), is generated and held by the application. That defeats the custody model of BRC-100 wallets: the application holds signing material that controls on-chain outcomes, the user cannot audit or revoke it through the wallet, and the key is lost with the application state. A wallet can sign ECDSA under BRC-100's createSignature, but neither createSignature nor any other BRC-100 method can produce a Rabin signature, and neither getPublicKey nor createSignature can carry a Rabin value: a getPublicKey result must be a 33-byte secp256k1 point, and a createSignature result must be DER of at most 72 bytes. Only encrypt and decrypt carry arbitrary byte arrays through every BRC-100 substrate.

This specification therefore adds a semantic operation behind BRC-98's reserved dispatch point, carried on encrypt, without changing BRC-100. It fixes every step that an implementation might otherwise choose differently, from seed expansion to the byte layout of the result, so that two conforming wallets holding the same root key produce the same Rabin public key and the same signature, and so that a contract author can embed a wallet's Rabin public key in a locking script and know that the wallet's signatures will verify under it.

Relationship to BRC-98 and BRC-2

BRC-98 reserves protocol identifiers beginning with p , requires unsupported wallets to reject them, and permits a supported scheme to define its own rules for permitted operations, key IDs, counterparties, permission attributes, and execution.

The rabin scheme uses that reserved dispatch point to specialize the meaning of an existing BRC-100 method, in the same way that BRC-229 specializes getPublicKey. This specification does not amend BRC-98 or BRC-2.

The semantic boundary is safe for compatibility:

  1. Outside p rabin, encrypt retains its ordinary BRC-2 meaning.
  2. Inside p rabin, a wallet either implements this scheme or rejects the request as BRC-98 already requires.
  3. The ciphertext returned under p rabin is not a BRC-2 ciphertext. It MUST NOT be passed to decrypt, and a conforming wallet rejects decrypt under p rabin.
  4. Applications never infer support from a new optional wallet member. They use a configured supporting wallet or handle the standard unsupported-scheme error.

Specification

Scheme and method

The BRC-98 scheme ID is:

rabin

A conforming implementation MUST accept this scheme only through the existing BRC-100 encrypt method. It MUST reject every other BRC-100 method requested under p rabin, including decrypt, getPublicKey, createSignature, verifySignature, createHmac, verifyHmac, and the key-linkage methods. The rejection of decrypt is load-bearing: Rabin decryption is the computation of a modular square root of a caller-chosen value, and a caller who obtains square roots of chosen values can factor n (see Security).

The protocol-name component of EncryptArgs.protocolID has this grammar:

p rabin <operation> <bits> <logicalProtocolID>

where:

  • operation is exactly key or sign;
  • bits is exactly 2048, 3072, or 4096, the size in bits of the Rabin modulus n;
  • logicalProtocolID is the application protocol whose Rabin key universe is being requested.

The outer BRC-100 call remains:

wallet.encrypt(
  {
    protocolID: [
      securityLevel,
      `p rabin ${operation} ${bits} ${logicalProtocolID}`
    ],
    keyID,
    counterparty,
    plaintext,
    privileged,
    privilegedReason,
    seekPermission
  },
  originator
)

keyID, counterparty, privileged, privilegedReason, seekPermission, and originator retain their existing BRC-100 meanings and locations. They MUST NOT be duplicated or encoded into the protocol string.

counterparty defaults to self under the existing method rules. self, anyone, and a canonical compressed public key are all permitted.

plaintext is the message M for sign. For key it MUST be empty (zero bytes); a non-empty plaintext with key MUST be rejected.

Logical protocol ID

The logical protocol ID MUST:

  • contain only lowercase ASCII letters, numbers, and single spaces;
  • contain no leading, trailing, or repeated spaces;
  • be at least 5 characters and no more than 272 characters; and
  • not end with protocol.

The 272-character ceiling ensures that the canonical derivation protocol p rabin <logicalProtocolID> remains within BRC-43's 280-character protocol-ID limit.

The complete outer p rabin string is a BRC-98 dispatch envelope, not the protocol ID used for BRC-43 derivation. It MUST be no more than 290 characters: a key request can be at most 289 characters and a sign request can be at most 290 characters. All permitted characters are ASCII, so these character and byte counts are identical. Both bounds remain within BRC-100's 400-character protocol-string limit.

A key ID MUST be 1 to 800 bytes of UTF-8. This is counted in bytes, not characters: it is at least as strict as the 800-character check that @bsv/sdk clients apply, and within BRC-43's 1033-byte limit, so a key ID the module accepts is never rejected by a conforming client. Non-ASCII key IDs are permitted; a module MUST apply the byte limit itself rather than rely on the client's character count.

Canonical seed derivation

Let:

  • s be the security level in the outer protocolID tuple;
  • L be logicalProtocolID;
  • K be the separately supplied keyID;
  • C be the separately supplied counterparty, defaulting to self; and
  • b be bits.

The module MUST derive a private key d using BRC-42/43 with:

protocolID   = [s, "p rabin " + L]
keyID        = K
counterparty = C

and define the 32-byte seed:

seed = d encoded as a 32-byte big-endian unsigned integer

The operation and bits MUST NOT form part of the BRC-42 derivation identity. One BRC-42 child therefore seeds the keys of all three sizes, and key and sign address the same key.

The seed MUST be obtained through private-key derivation (BRC-42 child private key: root plus HMAC offset). It MUST NOT be obtained from a BRC-2 symmetric key, an HMAC key, or any other value computed from the ECDH shared secret alone, because for a non-self counterparty the counterparty can compute those values and would thereby obtain the Rabin private key.

The p rabin prefix in the canonical derivation protocol isolates Rabin seeds from ordinary application keys using the same logical protocol and key ID. The derived scalar d is used only as HKDF input keying material; it is never used as a secp256k1 key and is never returned.

Key generation

Key generation is a deterministic function of (seed, b). Let k = b / 2 be the bit length of each prime.

Seed expansion. Using HKDF with SHA-256 (RFC 5869):

PRK = HKDF-Extract(salt = UTF8("p rabin v1"), IKM = seed)
OKM = HKDF-Expand(PRK, info = UTF8("p rabin v1 " + b), length = k / 4 bytes)
Xp  = OKM[0 .. k/8)          (the first k/8 bytes)
Xq  = OKM[k/8 .. k/4)        (the remaining k/8 bytes)

where b is written in decimal ASCII with no padding, so the info strings are exactly p rabin v1 2048, p rabin v1 3072, and p rabin v1 4096.

Candidate construction. For each of Xp and Xq, interpret the bytes as a big-endian unsigned integer x and set:

x = x OR 2^(k-1) OR 2^(k-2) OR 3

so that x has exactly k bits with its top two bits set and x ≡ 3 (mod 4).

Primality test. An odd integer x is accepted as prime if and only if both conditions hold:

  1. x is not divisible by any prime less than 2^16 (the 6542 primes 2 through 65521); and
  2. x passes the Miller–Rabin test for each base a in the first 32 primes (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131), where the test for base a is: write x − 1 = 2^s · d with d odd; compute y = a^d mod x; the test passes if y = 1 or y = x − 1, or if any of the s − 1 successive squarings y = y² mod x yields x − 1; otherwise it fails.

Both conditions are normative. The Miller–Rabin test with base a already rejects every multiple of a (then a^d mod x = 0), so the 32 bases cover every prime factor up to 131 by themselves. Condition 1 is decisive only for a composite that has a prime factor in the range (131, 2^16) and is nevertheless a strong pseudoprime to all 32 bases. No such integer is known, and for candidates of 1024 bits and above the heuristic probability is negligible, but an implementation that omitted trial division could in principle accept a candidate that another rejects. Condition 1 is therefore part of the definition so that implementations cannot diverge; in practice it is also the cheap filter that avoids most exponentiations.

Search. Starting from the constructed x, test x, x + 4, x + 8, ... and take the first value accepted as prime. The step of 4 preserves x ≡ 3 (mod 4). If the search would reach x ≥ 2^k, key generation MUST fail.

Let p be the result for Xp and q the result for Xq.

Rejection rules. Key generation MUST fail if p = q, or if |p − q| ≤ 2^(k − 100). Each of these events has negligible probability for an honestly derived seed; the rules exist so that every implementation makes the same decision.

Modulus. n = p · q. By construction n has exactly b bits, so n > 2^256 for every supported size. This bound is required by the on-chain verification equation below, whose right-hand side is not reduced modulo n.

Because p ≡ q ≡ 3 (mod 4), square roots modulo p and q are computed by a single exponentiation each. Implementations MUST NOT use any other prime shape.

Signing

Let M be the plaintext bytes and define the digest value:

h = LE_uint256(SHA-256(M))

that is, the 32 digest bytes read as a little-endian unsigned integer. This matches Bitcoin Script's reading of the OP_SHA256 output as a number. h < n always holds for the supported sizes.

The signature is the pair (sig, padding) defined as follows. For u = 0, 1, 2, ... up to 65535:

  1. Let t = (h − u) mod n, taken in [0, n).
  2. If t mod p ≠ 0 and t^((p−1)/2) mod p ≠ 1, continue with the next u. Likewise for q. (Euler's criterion; t ≡ 0 counts as a residue with root 0.)
  3. Compute the principal roots sp = t^((p+1)/4) mod p and sq = t^((q+1)/4) mod q.
  4. Let sig be the unique integer in [0, n) with sig ≡ sp (mod p) and sig ≡ sq (mod q) (Chinese remainder theorem).
  5. Set padding = u and stop.

If no u below 65536 yields a residue, the wallet MUST fail. (Each u succeeds with probability about 1/4, so this is negligible.)

The result is a deterministic function of (p, q, M): the same request always returns the same (sig, padding). The wallet MUST return exactly this pair. In particular it MUST NOT return n − sig or either of the other two square roots, and it MUST NOT return a larger padding when a smaller one is valid. A wallet that ever returned two roots r, r' of the same t with r' ≢ ±r (mod n) would reveal gcd(r − r', n), a factor of n.

Signing is hash-then-sign only. No operation accepts a caller-supplied value to be square-rooted, and no operation reveals p, q, sp, sq, or intermediate values.

Verification

The verification equation, as enforced by Bitcoin Script and by off-chain verifiers, is:

0 ≤ padding < 65536
(sig² + padding) mod n == LE_uint256(SHA-256(M))

where the right-hand side is not reduced modulo n. The padding bound is essential: without it sig = 0, padding = h would verify any message. Off-chain verifiers SHOULD additionally require 0 ≤ sig < n, which the wallet's output always satisfies.

This specification defines no wallet-side verify operation. Verification needs only the public values (M, sig, padding, n) and no wallet material; an application or a contract verifies directly. A conforming implementation MUST reject an operation token other than key and sign.

Result encoding

Results are returned as the ordinary encrypt result shape:

{ ciphertext: Byte[] }

The byte array is a sequence of fields. Each field is a 2-byte little-endian unsigned length followed by that many value bytes. Every value is a minimally encoded non-negative Bitcoin Script number:

  • the integer 0 is the empty byte string;
  • otherwise, the little-endian magnitude with no trailing zero byte, followed by a single 0x00 byte if and only if the most significant magnitude byte has its high bit (0x80) set.

The key result has one field:

Field Value
1 n

Because the top bit of n is always set, this field is always exactly b/8 + 1 bytes (257, 385, or 513), and the whole result is b/8 + 3 bytes.

The sign result has two fields, in this order:

Field Value
1 sig
2 padding

sig occupies b/8 or b/8 + 1 bytes; padding occupies 0 to 3 bytes.

These values are exactly the bytes that a Script push must carry for the on-chain verification sequence OP_MUL OP_ADD OP_MOD ... OP_NUMEQUAL to read them as the intended numbers under MINIMALDATA. When placing them into a script, the application MUST use the minimal push for the data: OP_0 for the empty value, OP_1 through OP_16 for a single byte 0x01 through 0x10, and a direct data push otherwise. In Rúnar terms, field 1 of a key result is the RabinPubKey constructor argument, and fields 1 and 2 of a sign result are the RabinSig and padding arguments of verifyRabinSig, with the same M as the msg argument.

Padding encodings that every implementation MUST produce:

padding bytes
0 (empty)
1 01
2 02
16 10
17 11
127 7f
128 8000
255 ff00
256 0001
32767 ff7f
32768 008000
65535 ffff00

A fixed-width or big-endian encoding is prohibited in the result. Two values that are equal as integers MUST produce byte-identical results.

Message size

A conforming implementation MUST accept a sign plaintext of up to 1 048 576 bytes (1 MiB) and MAY reject larger messages. The empty message is a valid message.

Privileged keys

The existing privileged and privilegedReason fields select privileged-key behavior.

When privileged is absent or false, the module derives d from the wallet's ordinary BRC-42/43 root.

When privileged is true:

  1. privilegedReason MUST be present and satisfy the existing BRC-100 description constraint (5 to 50 bytes).
  2. The wallet MUST obtain explicit authorization even at security level 0.
  3. The wallet MUST derive d from its privileged key material using the same canonical derivation tuple, and MUST generate the Rabin key from that seed by the same algorithm.
  4. The privileged root or key-derivation capability MUST NOT be exposed to the application.
  5. The wallet MUST fail closed if privileged derivation is unavailable.

A wallet implementation SHOULD request access to privileged material only after the operation and reason have been authorized, SHOULD release that access according to its existing privileged-key policy, and MUST NOT retain a privileged Rabin key in a cache beyond that policy's retention window.

Ordinary and privileged requests with otherwise identical arguments MUST produce different Rabin keys.

Permissions

BRC-98 dispatch bypasses a wallet's ordinary protocol-permission logic, so the module is solely responsible for authorization. It applies BRC-43 permission semantics to the logical Rabin protocol:

  • an ordinary security-level-0 request does not require a prompt;
  • a security-level-1 grant is scoped to the originator, logical protocol ID, and ordinary-versus-privileged root selection;
  • a security-level-2 grant additionally distinguishes the counterparty; and
  • ordinary and privileged grants MUST NOT satisfy one another.

Level 0 is permitted for signing for the same reason BRC-43 permits level-0 ECDSA signing: a Rabin signature has no authority of its own. It affects the user only once the user has locked funds to a contract that names this key's n, and that action is a separately authorized createAction. Wallets that nevertheless prefer a first-use prompt for level-0 Rabin keys MAY present one.

A wallet MAY cache a successful grant according to its normal permission lifecycle. The operation, key size, key ID, and for sign the message length and SHA-256 digest SHOULD be displayed or made available to the authorization UI, but they do not change the BRC-43 protocol-level grant scope. A trusted UI SHOULD describe the grant as the application being allowed to use a Rabin signing key under the logical protocol, not as encryption.

If permission is required and no applicable grant exists:

  • seekPermission: false MUST fail without prompting;
  • otherwise the wallet MAY seek authorization through its normal trusted UI.

Concurrent equivalent requests SHOULD share one pending authorization decision so that high-volume protocols cannot produce duplicate prompts.

Key material MUST NOT be derived or generated before authorization succeeds. This matters more than for other modules because key generation is the expensive step; an unauthorized originator must not be able to spend the wallet's CPU.

Display of application-controlled text. The logical protocol ID, key ID, and privileged reason are chosen by the application. A trusted UI that shows them SHOULD neutralize ASCII control characters and Unicode format and bidirectional-override characters (for example by escaping or replacing them), SHOULD truncate or wrap over-long values, and SHOULD render them in a way that is visually distinct from the wallet's own text, so that an application cannot compose a prompt that reads as a wallet statement.

Scheduling, caching and resource use

Generating a key is the expensive step: in a scripting runtime a 2048-bit key takes a few hundred milliseconds, a 3072-bit key about a second, and a 4096-bit key several seconds, with a long tail. A wallet that generated keys synchronously on its user-interface thread could be frozen by an application that requests fresh level-0 keys. The following requirements bound that.

Responsiveness. Key generation MUST NOT block the host's user-interface or event thread for the duration of a generation. Implementations MUST either run generation on a separate thread or worker, or run it cooperatively in bounded slices between which control returns to the event loop. A slice SHOULD be at most about 10 ms (one Miller–Rabin exponentiation of a 2048-bit candidate, or a bounded batch of trial divisions). A cooperative implementation MUST produce exactly the same keys as a synchronous one.

Concurrency. Implementations MUST bound the number of generations in progress and waiting. At most one generation SHOULD run at a time per wallet (on a single thread, concurrent generations only lengthen each other), and no more than a small fixed number of distinct keys (8 is suggested) SHOULD wait. Requests for a key that is already being generated MUST join that generation rather than start another. A request that cannot be queued MUST fail immediately with an error that distinguishes "busy" from denial and from invalid input, and MUST NOT wait. A request that waits longer than a fixed bound (60 s is suggested) SHOULD fail the same way.

Rate limiting. Wallets SHOULD rate-limit key generation (cache misses), not cached requests or signing. Limits SHOULD apply per originator and globally; 4 generations per minute per originator and 10 per minute in total, with bursts of the same size, are suggested. The limit MUST be checked after authorization and before derivation, so that a throttled request neither derives a seed nor leaves a cache entry, and MUST NOT consume a prompt. Wallets whose originator identity is an unauthenticated claim (for example an HTTP Origin header on a local port) MUST NOT rely on per-originator limits alone, because a local process can present a fresh originator for every request; the global limit and the concurrency bound are what protect such a wallet.

Caching. Implementations SHOULD cache (p, q, n) in memory, keyed by the complete derivation identity (root selection, s, L, K, C, b), and SHOULD coalesce concurrent generation of the same key. The cache MUST be bounded, MUST be cleared when the wallet locks or the module is disposed, and MUST NOT be written to persistent storage unless encrypted under the wallet's own key-material protection. The cache key MUST NOT be the seed itself. So that one originator cannot evict every other application's keys, wallets SHOULD cap the share of the cache that keys generated for a single originator may occupy (8 of 32 is suggested) and evict that originator's own least-recently-used key first.

First-use prompts. A wallet MAY require a trusted-UI confirmation the first time a level-0 key is generated for an originator, as an additional throttle. This is not required, it does not change the level-0 rule for sign, and wallets that implement the bounds above do not need it.

Baskets and labels

This scheme defines no output baskets and no action labels. A conforming implementation MUST reject every basket or label beginning with p rabin in createAction, internalizeAction, listActions, listOutputs, and relinquishOutput, and MUST NOT pass such a request through to the underlying wallet. This is load-bearing in wallets whose BRC-98 dispatch hands p -prefixed baskets and labels to the scheme's module instead of applying the ordinary basket and label permission checks: a module that merely passed them through would turn the reserved namespace into a permission bypass.

Counterparty validation

When counterparty is supplied as a public key, the same canonical validation as BRC-229 applies: exactly 33 bytes as 66 lowercase hexadecimal characters, prefix 02 or 03, x-coordinate less than the field prime (checked before any reducing parser), a point on the curve, and finite. The special values self and anyone remain valid.

Errors

A conforming wallet MUST fail without deriving, generating, or signing when:

  • the installed method is not encrypt;
  • the protocol grammar or any field constraint is invalid, including an unsupported bits value or an unknown operation;
  • plaintext is non-empty for key, or exceeds the implementation's accepted size for sign;
  • the public-key counterparty is invalid;
  • authorization is unavailable or denied;
  • privileged derivation is requested but unavailable;
  • the originator is missing; or
  • a basket or label beginning with p rabin is used in any method.

It MUST fail without deriving when a generation cannot be queued or is rate-limited, with an error distinct from denial and from invalid input.

It MUST fail after generation but before returning a result when key generation hits a rejection rule or no padding is found.

Errors SHOULD identify the unsupported scheme, invalid field, or denied capability without revealing key material, and MUST NOT reveal whether a cached key existed.

Conformance vectors

The vectors below pin seed derivation, seed expansion, prime search, signing, and result encoding. They were generated with the BRC-42 derivation implemented in @bsv/sdk (KeyDeriver.derivePrivateKey), which is cross-tested against the BRC-42 specification vectors, and independently reproduced from the seed by a second implementation. Every signature below has additionally been verified in a Bitcoin Script VM under the verifyRabinSig sequence emitted by the Rúnar compiler, with the wallet's result bytes used directly as the sig and padding pushes and the key result used as the RabinPubKey.

All vectors share these parameters:

  • security level: 2
  • logical protocol ID: example oracle
  • key ID: feed 1
  • canonical derivation protocol: p rabin example oracle
  • BRC-43 invoice number: 2-p rabin example oracle-feed 1
  • HKDF salt: p rabin v1

Test wallets use transparent, minimal root keys:

wallet A root private key:  0000000000000000000000000000000000000000000000000000000000000002
wallet A identity key:      02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
wallet B root private key:  0000000000000000000000000000000000000000000000000000000000000003
wallet B identity key:      02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9

Integers p, q, and n are shown as big-endian hexadecimal of exactly k/8 or b/8 bytes. Results are shown exactly as the ciphertext byte array.

Vector K1 — 2048-bit key, counterparty self. Wallet A receives:

protocolID   = [2, "p rabin key 2048 example oracle"]
keyID        = "feed 1"
counterparty = "self"
plaintext    = (empty)

Derivation and expansion:

seed (d, 32 bytes BE):
fdac63d5e7c922049e7b231cf6ec4ead2c36e16cc80f64162995573c31c25f9a
HKDF info: "p rabin v1 2048", OKM length 256
OKM:
1afc2bf10b574b28970a9a64f98012aea18e88793d019a87bb28a2b6673cdd85d79b4223b857a507f05715a3f15db2b37f80c30e93104db1b3ee5a5fa3c9f4d03c082a38e74cb12abf0ab60bb51455e562ed70cec67e1cb3fc57b4c891696b83ec4c4dda596616e6c2298fbf4784e0bef493ec1ddf1ee20a96d6cee5d58047e871b538916b9b3e67a171e5bc82005b40b43e6c29460eefc775085d822417d881916e501a3993b2a45c86dfcc5d772dafd1378baf74a798b3be087c43d3dafeec531f9ebec12228dd3385784fd89d87d72627b8e357be23b8281d4d1683eef1f34b83879398356808f33f731382c19f51c17e79b5ca0299724ee3d392d949f490

The constructed starting candidates are Xp and Xq with the top two bits and the low two bits set (da…7eb and f1…493). The first accepted prime is reached after 776 steps of 4 for p and 324 steps for q:

p:
dafc2bf10b574b28970a9a64f98012aea18e88793d019a87bb28a2b6673cdd85d79b4223b857a507f05715a3f15db2b37f80c30e93104db1b3ee5a5fa3c9f4d03c082a38e74cb12abf0ab60bb51455e562ed70cec67e1cb3fc57b4c891696b83ec4c4dda596616e6c2298fbf4784e0bef493ec1ddf1ee20a96d6cee5d580540b
q:
f1b538916b9b3e67a171e5bc82005b40b43e6c29460eefc775085d822417d881916e501a3993b2a45c86dfcc5d772dafd1378baf74a798b3be087c43d3dafeec531f9ebec12228dd3385784fd89d87d72627b8e357be23b8281d4d1683eef1f34b83879398356808f33f731382c19f51c17e79b5ca0299724ee3d392d949f9a3
n:
cec26a0c8b9802e9f1a9a75615509f1b7a1de78f5ee03c822e05615b2472b67f3a2d7d667da096940596b215f8f6a1caab123d8c1e6217e799ac3c552c648aa2d0d17ed5d4fcf1cbb5fab897f5e863ee4d20f631ba7ae3c9de726342d6087457ef5cd3b80251e84b8723a37fda9babe5485dd2580dd24de9dfc055f44c778f2ba635e835a5ccfc1b27fed22cfea951153341156dadcf32c5f3bbb36763bcebb6fdcbf93bf098cf0c5a0132f39acc38f91a1e9ccb5f75abc660d8eda6e9fc446f889eceb770047c742a62e20e20d0998cf22e24f8f510da5eba6728935b00ee5056c5dd1f6e9a1d66bc57bfa9093755356834155f34c02a6f33bcf11c0c973601

Result (259 bytes: length 0101 = 257, then n little-endian with a 00 sign byte):

01010136970c1cf1bc336f2ac0345f15346835553709a9bf57bc661d9a6e1fddc55650ee005b932867ba5eda10f5f8242ef28c99d0200ee2622a747c0470b7ce9e886f44fce9a6edd860c6ab755fcb9c1e1af938cc9af332015a0ccf98f03bf9cbfdb6ebbc6367b3bbf3c532cfad6d1541331551a9fe2cd2fe271bfccca535e835a62b8f774cf455c0dfe94dd20d58d25d48e5ab9bda7fa323874be85102b8d35cef577408d6426372dec9e37aba31f6204dee63e8f597b8fab5cbf1fcd4d57ed1d0a28a642c553cac99e717621e8c3d12abcaa1f6f815b296059496a07d667d2d3a7fb672245b61052e823ce05e8fe71d7a1b9f501556a7a9f1e902988b0c6ac2ce00

Vector K2 — 3072-bit key, same seed. With bits = 3072 and everything else as in K1, the seed is identical and only the HKDF info (p rabin v1 3072, OKM length 384) differs:

OKM:
551267916ef8d30d9020a07f330977034b666009dfa978d8726ffdefdab0733475589a37a96e0f916f42953720f157b42b5890245c5dc97bb02d58a970770b4d2ce5214a22249343746ff91991ac332480ce933f8cd5048439e7f76b494ce910e6b5b7b2ad73caf0824d823241ab941b75d071e0f59d87f9acec28d7858e99c3cda0878f41437f277c267c119d365d9244ddb112c54005417bebb1a8c463004c0cf332d49a56262e01bb2473cebdf83de62a3f54876286034520017e616fe18d53fe2ce3856a95f6b0b7382ef11bccb53a8309993a796c3de2f621ff19e81a7798a34ee6db897e9d28e71db85abd6685d45bcb4fd145fbc5356cf58ba8daa546188144578e817bac0d363414c9943e5ffdaf2e801d6266fd72cbeaaaf4ce7b2b5e6ab79936de435140f00bdfb653398e10aea4bcac1c2e83f3dc3e33e6f9ac7c81059aa2f9a685ca0b7aa449786fe9b54296c4f92aa70b9e34aaf77bce07c9934783005d5c5242ac758c95e83b121d5544e5d5ca520314707f457c5ff93c37c9
p (263 steps):
d51267916ef8d30d9020a07f330977034b666009dfa978d8726ffdefdab0733475589a37a96e0f916f42953720f157b42b5890245c5dc97bb02d58a970770b4d2ce5214a22249343746ff91991ac332480ce933f8cd5048439e7f76b494ce910e6b5b7b2ad73caf0824d823241ab941b75d071e0f59d87f9acec28d7858e99c3cda0878f41437f277c267c119d365d9244ddb112c54005417bebb1a8c463004c0cf332d49a56262e01bb2473cebdf83de62a3f54876286034520017e616fe5ab
q (87 steps):
d3fe2ce3856a95f6b0b7382ef11bccb53a8309993a796c3de2f621ff19e81a7798a34ee6db897e9d28e71db85abd6685d45bcb4fd145fbc5356cf58ba8daa546188144578e817bac0d363414c9943e5ffdaf2e801d6266fd72cbeaaaf4ce7b2b5e6ab79936de435140f00bdfb653398e10aea4bcac1c2e83f3dc3e33e6f9ac7c81059aa2f9a685ca0b7aa449786fe9b54296c4f92aa70b9e34aaf77bce07c9934783005d5c5242ac758c95e83b121d5544e5d5ca520314707f457c5ff93c3927
n:
b071b8fc28ed4c1c3a14e898811f3b0e9ced3ffc896925162aa0241c2bafc49a990956d7230a37ae15deba91dc56bf3b88ed3d449b08651bcc54331859de0801756907b6986ebe31870e539a61e49fd69be3382ee82d751b905a69a4c0340e8ce70a1c7f1bb101c3bf47d49c19b20587b5f0b504a13489a09c195c7eca3ca870a000948ffeeb745db67d16e17b44f3c755f32e7de5ca9e9152cb13eff49a9d507b214db2ec2d223a7b31affb984507308461b07321e14d9bf063af78fcec300f29095d1a6a72107da0d05a43bb60d1d0d798b045f5403afe8c91545a3050e0c5df30631ede8bb02ddfcc527b44ada0f4e4e9d20c7bba49aa5dde14e0d0ccb2a8e6589d93fc68c9844095ab11394c6777bb06d75bbcfd43bebd1be535b51aa685440d4304e6377d351dfeb299f806e556072c51c149ba16daca639925e594f1d84e9eb1f3dd3f8c3abf17720af48b441656208f013d07899c6fa4dc6110a2a3b66fd79f2a534629e6d3840a7146453d8454d6dd3c3dc0dbd906761904e943100d

Result (387 bytes, length 8101 = 385):

81010d1043e904197606d9dbc03d3cddd654843d4546710a84d3e62946532a9fd76fb6a3a21061dca46f9c89073d018f205616448bf40a7217bf3a8c3fddf3b19e4ed8f194e5259963cada16ba49c1512c0756e506f899b2fe1d357d37e604430d4485a61ab535e51bbdbe43fdbc5bd706bb77674c3911ab954084c968fc939d58e6a8b2ccd0e014de5daa49ba7b0cd2e9e4f4a0ad447b52ccdf2db08bde1e6330dfc5e050305a54918cfe3a40f545b098d7d0d160bb435ad0a07d10726a1a5d09290f30ecfc78af63f09b4de12173b0618430074598fbaf317b3a222decb24d217b509d9af4ef13cb52919ecae57d2ef355c7f3447be1167db65d74ebfe8f9400a070a83cca7e5c199ca08934a104b5f0b58705b2199cd447bfc301b11b7f1c0ae78c0e34c0a4695a901b752de82e38e39bd69fe4619a530e8731be6e98b60769750108de59183354cc1b65089b443ded883bbf56dc91bade15ae370a23d75609999ac4af2b1c24a02a16256989fc3fed9c0e3b1f8198e8143a1c4ced28fcb871b000

Vector K3 — counterparty separation. Wallet A with counterparty set to wallet B's identity key, bits = 2048, all else as in K1, derives a different seed and key:

seed:
62447634b84f19a5d8f83402b6b81d2a4a7884fe3d3604016bb2a998cef3b63b
p (190 steps):
e4f26ecbfd854e197845a896b8548368911b4c6dd5ec2b00f6cb810ca2a2557fb7f800a56108bbcf8bfabe97c04c6dbab2a4cc4317645479d71e62d3c83692b01a2925c1c54037a90f2680cc1c269504088b57bf4765f55f738332eabe44a88176495e0ad1901b78605f84ac837257838b6c8b606363e75b91b95b9c3d12b95b
q (214 steps):
fe57dec2e0652a13a3ef9232c87ec0b960b29a5209498ae25084c9a07154c14f914794188c4e32315bf52c18489978e6d5c78f4af095a8a86eb57a99a07d70baf7af518ae2d7c4485e124bf2484aabd27059f796acd67d5476ca1325785fb68079b69d8eabcaa24ea1f332fe37898e024c6878fe6d2430843be1485e92e76f6b
n:
e3771f8a9158e27413d43ad62ccda2a351439e31515af0af724f156035073794b521ffb9f11c31ad8c015849a154dce8bdf14a0cebce4664a3709e6a09f800063fef35c323493b26aebdfe8246c1d08ca797b37941c9d000db6dd8108403bd0a898609a10fbd6d590863ea725603deb73830389c0299e5c2f93a515486ccd75c8fa45c1494d437fbdc4cefdebb8c3471554871b789d067b70bacf10639a9874254e401b8f710f595a7cc442c258a2f05afa38d9bdb93de25066829540e6d5098ca4406b5afbe56bf862691effe9e02cf85455454a51f93d563268e5a17b74d4b6447dd734cb2f972ee97af01208f4bf11a6fddf6d33bd19a9efe79cdcc4eee09

Result:

010109ee4ecccd79fe9e9ad13bd3f6dd6f1af14b8f2001af97ee72f9b24c73dd47644b4db7175a8e2663d5931fa554544585cf029efeef912686bf56beafb50644ca98506d0e5429680625de93db9b8da3af052f8a252c44cca795f510f7b801e4544287a93906f1ac0bb767d089b771485571348cbbdeef4cdcfb37d494145ca48f5cd7cc8654513af9c2e599029c383038b7de035672ea6308596dbd0fa10986890abd038410d86ddb00d0c94179b397a78cd0c14682febdae263b4923c335ef3f0600f8096a9e70a36446ceeb0c4af1bde8dc54a14958018cad311cf1b9ff21b59437073560154f72aff05a51319e4351a3a2cd2cd63ad41374e258918a1f77e300

Vector S1 — sign, K1, message abc. Wallet A receives protocolID = [2, "p rabin sign 2048 example oracle"], plaintext = 616263, other arguments as in K1.

SHA-256(M): ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
h (decimal): 78287233127892744762064256752147977895291281102938609026632374073081434896570
padding: 2

Result (261 bytes: 0001 = 256-byte sig without sign byte, then 0100 = 1-byte padding 02):

0001aa7034f633b00a326dc55fc7cf1b45f5553f55d0ab0011c6fc7f60b58816b0f908002881156b2ebf573db5f9c129cdda4353a7661eb1312d2e067f259702498184bf715555609d45cf80a5d2b17c0112e3eeb302462f3674c055d97dee7fec42fea7cb04e400cbcbbea24e57dd1e52166f3430ea928e8e51badf81752d7150d6ed5591130a0be2a027296526be211fb4913f7d6c31468e7275efca9a46d7b5fa15857763fdeebc98011a231227483fbf42a8f141ff3a77969b6c97d2d511aaf45ae146eaf1dfbe3d28a74039298f9b0c296602d7d28f1a624405f9a6ae5d6a7640cee4dbd5efcbd803798596617f8f2abe21ec962233542dc870fb6e9ee2cf1d010002

Vector S2 — sign, K1, 32-byte message 000102…1f. plaintext = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f.

SHA-256(M): 630dcd2966c4336691125448bbb25b4ff412a49c732db2c8abc1b8581bd710dd
padding: 4

Result (262 bytes; sig is 257 bytes because its top magnitude byte is 0xfe, so a 00 sign byte is appended):

0101a08fb02dce64979bea39c65e92b322c0142472f25f58f3f40d30007aa4531bb21c169b48666208eb4782b67241911f8753cd2dac348b1b8b9c20e93a5dc93b3c0c947635d4f9d50e6b359e23814e27cdb7efe71e9c714ab31c6c2b644e8c0ec391a625862ecc221966187c12f6982d6d19bcc161c1bf50ef26c4ec7f8442986eaf664f757b7f0380a94a5f27b4def44799a9dcf99109f6567d0b823bd2a10577111391e6d9e938b3c00261dd2dacf9f6db7ea9eaa7ed336ffff778930fd1d941d7d06a3dcc9bc449dd3b796c4d708af7d32be53f0008aef6987fcaa51f6b1e46fd9b128c271c6962ec6ac44a80ec7986ad8e672afab1247d665290e3ec49feb100010004

Vector S3 — sign, K1, empty message. plaintext is empty.

SHA-256(M): e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
padding: 5

Result:

010194540cf3bc1946b9895f10fd36b60b44cefc6fff542a8331b9514e24ef8e6c0883ba454b19b5a04b2a947dabf55cbda09cb4413964df3ca7e6cee9bad2d893e54fe9de1d9d5027602c5be88ee0f10863d8dde515c5916bf9b8416a21356e00bbcfacf203a180979224c892df7c700ebf2937d50318f6ea66b836b28167014da025cf2de5811ff81c46a2f5e841c07cb6c95cfecc39cbc212554bff46759985f813f6e00a8730ce5b68e39777a2f13c16bc55b43e1cc87b4a3bde307e30c8daa9bd8dbd044214619ac5cdc3f50334630fe225ad503cb63d65835be96099ec49d193c490627d3cee9cf2671b71aed5739ab8e956a040144fc61246590ed43917c600010005

Vector S4 — sign, K2 (3072-bit), message abc. protocolID = [2, "p rabin sign 3072 example oracle"].

padding: 2

Result (390 bytes; sig is 385 bytes):

81016cbe54ec1f7557969c667934b9083f917602bb776171c05f3ab4a9e018c7f7e6c8880632e3efdb63f5f1ec0cc512e6716f0c9d9dfdc40fb750d7739f3e218fa30289c906a7e12c0112ba2fd86717202815d96a964ac1ec9d8479a52a4017eeed012d2509c2dfd42f8eae13dc5afe4de5d84a3a5c9d5e073b56a779b969518e88f1206d3a35a310c6d433e0a0b51f68096de3895489f8f0f2d8e092bd6a2d98e826eb54f8270cc10dce85df9452a549ccea0189b991c0f346103a9e3f7218d2db7c19133542d9a7a3e250bb323160fe6ff3344964d66de893c0945ffab8b3708e3901cd2a4ebc39269b9e4260257eedeb802220b15f80afc6fa9f49be3fe7231887101e5a6772194d128c2143ab4f969cca76d479ab6327ec4d41d5274f4c206fcf0d141a3894a405d4708e1623292f7bac77d0c682df4d530adfffcb516f4d1e5788102128c2daba10a5d097f1db1b6ccf125b970cdcf292e8665452ef89e433d523cd81dea99ce3ac06589b129336431153522942587a58309eee297fa3c7ae00010002

Vector S5 — sign, K3 (counterparty B), message abc. The padding is 0, so the second field is empty and is pushed on chain as OP_0.

padding: 0

Result (260 bytes, ending in the empty field 0000):

000195ed00c5d54f2d9c62084dd7533569beaf8353c37603b0ad89a00c57edb2ccea913008a8a69fb3cd10aaf606ce76b82df3b70672a32ab1520b7757e6a07c8d42038a7e090cbf860a26e1cb88a55b57b98c508b1c91172cd43441211fc5aa61c51d22ccf65a0d6cb4308d5cbf90f8f58dbe2ec9fcd35eb7294afbf732e2cfa9b04f8c171be743d495f61ff709fc27287286766c0c9096cc6c6f370cd963b085d9b178fc9b6b2023bf56301a0cfae744e9c82df1fc21aca3a0ec53c20d1bfe9ccfc51d6c2df678e3f53f00d44e022feb81b21e5a06304317b9ca52be38f1b9cbdd50608c201937a3a6ee7522e46ca5536b40f21c018298199f9b73cfd10ea2d10a0000

Vector S6 — sign, K1, message msg 0 (6d73672030).

SHA-256(M): bf40f3ea98f97bcfdc40cec1c291497e2dd089d05653bd389189c57b6ece0aed
padding: 3

Result:

0101a30a5e9c903f8005414f94c93278632adc85fcb563b92e0868fb3f3fc01f88e2da87e90a689154434fcdbb4ed2ecc66adda1244aa08aa97dafe96f01744ab55c56170a958f7bfaf32e518326d7bd7f959274b0331272de2352e4762e6d95ccd87de410849d14774f5579753368a520e016d8aa12ed09a4b021a3d6fe61c5d70d055852d2d6690761f7ed89056aeffd3e13db2c8c33f3868f10a1cf9b8467cb0ca49cb12195b35da1b4a8446d7fd3b5c38dfaf8cbb3365a43546c8ded614b93a718da646c07510306422d02d60dabafe60300b52c215190ac79ffffe71afb4c7f6a695821644b30992445f6d72eb661d7fa5c271a17a984d6e7dd706e9bcd1fa600010003

Vector S7 — sign, K1, message msg 2 (6d73672032).

SHA-256(M): 4de0da51ade3b600bd18dad3e54c1e1e0471ca81e515691d18fb8141d1dc847c
padding: 1

Result:

0001e95fcaaa3771bcfeeb00729b4e4620cb62311039923de3861bc902728c0484b4ed7393be51c91a1255e3685ae8ac6b759476e303fced411929fc5aa13869a130c9e110b3b1b8964e8d063b0ed4ab9c2f4000727b7322e71a20065323dbff2cc7a26e76027040fa131660cf0483618608f3ff411e6c4d36e5ce3f34544c1acba2313e88e8c04c3688caa216aa860e619b07def08ffdcc8543af3db0373ca667e6c9deb5d6c8e785e1d18d23b8170e8b5c8a4e77d8320846ad20766bddbff41339c25e9ffa86a0f5bd6d49d46a072a9a718f658280dae9cb137005362473750fbb4da644c31e8f2571c42a11ee5fd93770273e0ed14a1eb8c773d5f9c232daf105010001

Vector R — rejected requests. Each of these MUST be rejected without deriving a key:

Request Reason
decrypt with protocolID = [2, "p rabin sign 2048 example oracle"] only encrypt is permitted; decrypt would be a square-root oracle
getPublicKey or createSignature under p rabin … not the carrier method
encrypt with p rabin verify 2048 example oracle unknown operation
encrypt with p rabin sign 1024 example oracle unsupported size
encrypt with p rabin sign 2048 Example Oracle uppercase in logical protocol ID
encrypt with p rabin key 2048 example oracle and a non-empty plaintext key takes no message
encrypt with p rabin sign 2048 example oracle and counterparty = "04…" (uncompressed) not the canonical 33-byte form
encrypt with privileged: true and no privilegedReason reason required

Privileged derivation (root-specific) and unsupported-module rejection are behavioral requirements without portable vectors: a wallet MUST produce different keys for ordinary and privileged requests with otherwise identical arguments, and a wallet without the module MUST reject the reserved protocol as BRC-98 requires.

Protocol-length boundaries are also normative: implementations MUST accept a 272-character logical protocol ID for both operations, including the resulting 290-character sign envelope, and MUST reject a 273-character logical protocol ID.

A machine-readable copy of these vectors, the generator, and an independent checker accompany the reference implementation.

TypeScript reference module

The reference implementation is the installable @bsv/rabin-permission-module package in bsv-blockchain/ts-stack (packages/wallet/rabin-permission-module), which BSV Desktop registers as its rabin permission module. It uses the Wallet Toolbox permission-module interface's optional semantic handler:

handleRequest?: (
  request: { method: string; args: object; originator: string },
  next: (args: object) => Promise<unknown>
) => Promise<unknown>

The Rabin module returns the result directly and never forwards its request to ordinary encrypt; forwarding would produce a BRC-2 ciphertext, not a Rabin value.

The Wallet Toolbox also routes p -prefixed baskets and labels of createAction, internalizeAction, and listActions to the scheme module's onRequest and onResponse hooks, bypassing its basket and label permission checks. The Rabin module's hooks therefore throw for every method instead of passing the request through; listOutputs and relinquishOutput reach handleRequest, whose method check rejects them.

Key generation in the reference module is a generator driven either synchronously (vectors, tests) or cooperatively with yields between slices, with one generation in flight, a bounded queue, per-originator and global token buckets for cache misses, and a per-originator cache share, as the scheduling section requires.

A wallet host installs it alongside other BRC-98 modules under the rabin scheme and supplies:

  • its ordinary BRC-42/43 key deriver;
  • its trusted authorization callback; and
  • optionally, a privileged-key-deriver provider.

The key derivers, the seed, and the primes remain inside trusted wallet/module code and are never included in the BRC-100 response. The module exports a pure verifier so that applications can check signatures without the wallet.

Security

Key isolation

The Rabin private key is derived from a BRC-42 child private key of the p rabin <logicalProtocolID> namespace and from nothing else. A wallet MUST NOT substitute its identity key, spending key, or a key from an ordinary non-Rabin protocol, and MUST NOT seed Rabin keys from any value a counterparty can compute. The method restriction is equally mandatory: decrypt under the same namespace would be a chosen-value square-root oracle, which is a factoring oracle for n.

revealSpecificKeyLinkage for a p rabin … protocol is outside this module. It returns the BRC-42 HMAC offset for the tuple, which any public-key counterparty can already compute from the shared secret, and which yields only the child public key d·G. d·G is unrelated to n: recovering d from it is the discrete-logarithm problem, and the seed enters key generation only through HKDF. A wallet MAY refuse linkage revelation for the p rabin namespace, but gains no security by doing so.

Canonical roots and padding

For a given t, four square roots exist modulo n. Any two of them that are not negatives of each other factor n. The signing procedure fixes the root (CRT of the principal roots) and the padding (the smallest valid one) as pure functions of (p, q, M), so a wallet cannot be induced to reveal a second root by repeating a request. Implementations MUST NOT randomize either choice.

Forgery resistance is bounded by the digest, not the modulus

The verification equation compares against the unreduced 256-bit digest value h and admits an additive padding below 2^16. An attacker who ignores the key entirely can set sig = ⌊√h⌋ and padding = h − sig²; this verifies whenever h − sig² < 2^16. Since h − ⌊√h⌋² ranges over about 2^129 values, each candidate message succeeds with probability about 2^-113, so an existential forgery costs about 2^112 to 2^113 hash evaluations for every supported modulus size, and no on-chain or off-chain verifier can tell such a forgery from a genuine signature. This is comparable to 2048-bit factoring strength (about 2^112), so 2048 bits is the balanced choice; 3072 and 4096 raise only the cost of factoring n and the size of the on-chain data, not the forgery bound. Applications that need more than about 112 bits of forgery resistance cannot obtain it from this construction.

A related chosen-message path targets the wallet rather than the verifier: find a message whose smallest valid padding leaves t = h − padding a perfect square s² of a small integer, have the wallet sign it, and compute gcd(sig − s, n). About one message in 2^126 qualifies once the padding rule is accounted for, so this costs about 2^126 hash evaluations and is dominated by the forgery bound above.

Prime generation

Candidates are derived from a secret seed, so an adversary cannot submit a pseudoprime to the primality test. For uniformly random candidates of 1024 bits and above, Miller–Rabin with 32 fixed bases after trial division has an error probability far below 2^-128; the bases are fixed rather than seed-derived so that every implementation accepts exactly the same integers. Incremental search from a random starting point is the standard construction used by common cryptographic libraries. Setting the top two bits guarantees n > 2^256, which the unreduced on-chain comparison requires, and guarantees a fixed result length.

On-chain malleability

The Script verification sequence accepts sig, n − sig, the other two roots, sig + m·n for any integer m, and any padding that happens to work. A third party who sees an unlocking script can therefore rewrite it, changing the spending transaction's ID if nothing else binds it. This is a property of the Script construction, not of the wallet, and contracts that need a stable spend SHOULD bind the unlocking data with an ECDSA signature over the transaction (OP_CHECKSIG), as any contract that spends funds already does. The wallet's output is canonical so that the honest path is deterministic.

Permission and denial-of-service considerations

Key generation costs from a few hundred milliseconds to several seconds per key, and a level-0 request needs no prompt, so an application can request fresh keys at will. Wallets MUST authorize before deriving, MUST keep their event thread responsive, MUST bound generations in flight and waiting, SHOULD cache keys with a per-originator share, and SHOULD rate-limit cache misses per originator and globally, as the scheduling section specifies. Signing itself costs two modular exponentiations and may be allowed at protocol-grant throughput. Wallets SHOULD make the logical protocol, originator, operation, key size, key ID, counterparty, message digest, and privileged reason available to trusted policy code.

Capability containment

The public application interface exposes only encrypt and receives only the public modulus or a signature. The semantic module is trusted wallet code: it may use internal key-derivation capabilities, but it MUST NOT return those capabilities, the seed, a prime, an intermediate root, or privileged root material. Implementations SHOULD zero cached primes when they evict them. Big-integer arithmetic in most scripting runtimes is not constant-time; implementers SHOULD be aware that signing time depends on (p, q) and avoid exposing fine-grained timing to untrusted callers where that is practical.

Why existing primitives are insufficient

BRC-100 createSignature produces ECDSA over secp256k1 only, and its result validation admits nothing but DER of at most 72 bytes. getPublicKey admits only a 33-byte point. No BRC-100 method or BRC-43 naming convention can make a wallet compute a modular square root under a wallet-held composite modulus, and no combination of existing methods yields a value that Script can verify over arbitrary data in a few dozen bytes.

Holding Rabin keys in the application is the status quo, and it is the problem: the key is outside the user's custody and recovery, its use cannot be permissioned by the wallet, and it is lost with the application. BRC-98 supplies the semantic dispatch; encrypt supplies a byte-carrying result that every substrate already passes through; BRC-42/43 supply a permissioned, recoverable, counterparty-separated seed. This specification supplies everything in between so that the result is the same in every conforming wallet.

References