BRC-229: Wallet-Native Elliptic Curve Point Multiplication - #230
BRC-229: Wallet-Native Elliptic Curve Point Multiplication#230connormurray2 wants to merge 2 commits into
Conversation
Adds one method, multiplyPoint, letting an application ask a BRC-100 wallet to multiply a caller-supplied secp256k1 point by a BRC-42/43 derived key without disclosing that key. An invert flag multiplies by the modular inverse, so a mask the wallet applies is a mask the wallet can remove. Motivation: commutative-masking protocols -- Barnett-Smart mental poker, verifiable shuffles, oblivious transfer -- rest on a.(b.P) == b.(a.P). BRC-100 exposes no way to reach the group operation, so an application needing it must generate and hold secp256k1 keys outside the wallet. In the mental-poker case those keys are exactly what privacy of the user's hand depends on. That is the outcome BRC-100 exists to prevent. No companion derivePoint method is proposed: getPublicKey already returns d.G for the same derivation arguments. The security section argues the method is not a signing oracle -- recovering the scalar is ECDLP, adaptively chosen points yield only what getPublicKey already gives, and a key that never signs has no signature equation to attack. It then names the risk that is real: d.Q IS the ECDH shared secret with Q, so implementing this over a spending or identity key would break BRC-2 encryption to that counterparty. Mandatory BRC-42/43 derivation is what prevents it, which is why that rule is a MUST and no identityKey option is offered. Validation rules are normative and drawn from implementation experience rather than asserted in advance. A canonical-encoding check on both coordinates is required and is NOT implied by an on-curve check: some libraries accept an x-coordinate greater than the field prime, reduce it silently, and then report the point as on-curve. The reference implementation shipped with exactly that hole until a test submitting thirty-two 0xff bytes caught it. Reference implementation and test suite in Go: https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dence Two corrections, both discovered by implementing the spec rather than by rereading it. multiplyPoint is now declared optional, with feature detection required. The first draft said wallets MUST expose it, which is unimplementable: BRC-100 is specified as an unchanging interface, so a method declared mandatory after the fact retroactively invalidates every wallet and substrate already shipped against it. Declaring it required in @bsv/sdk broke 23 call sites -- WalletClient, HTTPWalletJSON, WalletWireTransceiver, window.CWI, XDM, ReactNativeWebView, plus the KV store, registry and identity clients -- none of which have any reason to multiply an elliptic curve point. The new Optionality section states the rule and generalises it: any future BRC adding a method to this interface faces the same constraint. The canonical-encoding rule is upgraded from an anecdote to a table of measured behaviour. '02' + 'ff'*32 is accepted, silently reduced to 0x1000003d0, and then reported on-curve by BOTH @bsv/sdk (PublicKey.fromString / validate) and go-sdk (PublicKeyFromString / IsOnCurve). Two independently written libraries sharing the defect makes it an interoperability hazard rather than one library's quirk. The spec now also requires the range check to run BEFORE the parser, since the parser is what performs the reduction -- validating afterwards is too late, because the out-of-range value has already become a different valid point. Implementations section now cites the TypeScript reference implementation proposed at bsv-blockchain/ts-stack#487, verified against that repository's own tooling: tsc -b clean, oxlint --deny-warnings clean, 157 suites / 5924 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This should be reframed as a proposal to change the wallet interface rather than attempting to change BRC 100 itself, which is fixed by definition. |
|
I think the primitive is valuable, but I do not think it should add a new method or call code to BRC-100. This looks like exactly the kind of extension the BRC-98 P-protocol/module namespace was created to support. BRC-100 is intentionally fixed. Making Instead, I suggest reframing BRC-229 as the definition of a BRC-98 module scheme, perhaps BRC-229 itself can define the Proposed application-facing formFor example: await wallet.getPublicKey({
protocolID: [
2,
`p ecpm apply ${pointHex} mental poker deal`
],
keyID: 'deck-mask-1',
counterparty: 'self',
privileged: false,
seekPermission: true
})would return: {
publicKey: dP
}To remove the transformation: await wallet.getPublicKey({
protocolID: [
2,
`p ecpm remove ${maskedPointHex} mental poker deal`
],
keyID: 'deck-mask-1',
counterparty: 'self',
privileged: false,
seekPermission: true
})would return: {
publicKey: d⁻¹P
}The concrete grammar could be: where:
If retaining the shorter form is preferable, with an explicit form for removal. I slightly prefer always including The result still conforms to the existing Internal derivation semanticsThe point and operation must be excluded from the derived key’s identity. Given: the module should derive the scalar using a canonical module-specific derivation namespace equivalent to: protocolID: [
securityLevel,
`p ecpm ${logicalProtocolID}`
]
keyID
counterparty
privilegedIt then computes: This distinction is load-bearing. If The canonical derivation domain should nevertheless retain For privileged requests, the same operation should be performed using the wallet’s privileged derivation root, exactly as existing key-related methods already do. Applying and removing a privileged mask must use the same
Why this is an intended P-module useThe module system already routes P-protocol uses of It would be useful for this BRC to state explicitly that “transform” includes scheme-defined method semantics. A P-module is therefore not limited to deciding whether a normal BRC-100 operation is authorized. It may define what an existing method means inside its reserved namespace, subject to the method’s existing transport shape. That interpretation gives BRC-98 an important architectural purpose: it becomes the extension mechanism for wallet capabilities without changing BRC-100 itself. Otherwise the ecosystem will continue adding optional members and call codes whenever a new wallet-native operation is required. The current multiplyDerivedPoint({
protocolID,
keyID,
counterparty,
privileged,
point,
invert
})The module should receive this narrow capability, not unrestricted access to the wallet’s root key or raw This also fixes a key-isolation problemThe current security argument says that a key reachable through Without a module-specific namespace and method policy, an application can call Those methods derive the same private key. This matters because (dQ) is the ECDH shared secret for derived private key (d) and point (Q). The proposal correctly identifies key reuse as the real security risk, but the proposed standalone method does not itself prevent that reuse. An
Thus the module does more than preserve interface compatibility: it makes the proposal’s “this key is only used for point multiplication” security invariant enforceable. Clarification concerning
|
ty-everett
left a comment
There was a problem hiding this comment.
Suggested full replacement: specify BRC-229 as the ecpm BRC-98 semantic module rather than adding an optional BRC-100 method. This keeps BRC-100 and its Wallet Wire unchanged, retains key ID/counterparty/privileged controls in their existing fields, defines apply/remove over one canonical derived scalar, and explains precisely why naming the point as a pure BRC-43 counterparty is not equivalent. The companion reference implementation has been revised in bsv-blockchain/ts-stack#488.
| # BRC-229: Wallet-Native Elliptic Curve Point Multiplication | ||
|
|
||
| Connor Murray (connor.murray@bsvassociation.org) | ||
|
|
||
| ## Abstract | ||
|
|
||
| This proposal adds a single method, `multiplyPoint`, to the [BRC-100](./0100.md) wallet interface. It multiplies a caller-supplied secp256k1 point by a [BRC-42](../key-derivation/0042.md)/[BRC-43](../key-derivation/0043.md) derived private key and returns the resulting point, without ever disclosing the key. An `invert` flag multiplies by the modular inverse of that key, so a transformation applied by the wallet can be removed by the wallet. | ||
|
|
||
| This is the smallest addition that lets commutative-masking protocols — mental poker, verifiable shuffles, oblivious transfer, threshold key exchange — run inside a BRC-100 wallet instead of requiring an application to hold raw private keys of its own. | ||
|
|
||
| ## Motivation | ||
|
|
||
| BRC-100 gives an application a complete vocabulary for asking a wallet to act on the user's keys: sign a transaction, produce a signature, encrypt, HMAC, derive a public key. Every one of those operations is defined over data the wallet chooses to interpret. None of them expose the underlying group operation. | ||
|
|
||
| That gap has a concrete consequence. A class of multi-party protocols is built on the commutativity of scalar multiplication: | ||
|
|
||
| ``` | ||
| a·(b·P) == b·(a·P) | ||
| ``` | ||
|
|
||
| Barnett–Smart mental poker is the canonical example. Each participant masks every card with a secret scalar; because masking commutes, the participants can apply their masks in any order and later strip them in any order. No participant ever learns a card another participant was dealt, and no dealer exists to trust. This is the standard construction for trustless card games, and the same primitive underlies verifiable shuffles and several oblivious-transfer constructions. | ||
|
|
||
| A BRC-100 wallet cannot participate in any of it. `getPublicKey` returns `d·G` for a derived key — the generator multiplied by the scalar — but there is no way to ask for `d·P` for an arbitrary point `P`. So an application that needs commutative masking must generate and hold its own secp256k1 keys outside the wallet, in application storage, with application-grade key hygiene. | ||
|
|
||
| The result is the outcome BRC-100 exists to prevent. The user has a wallet that protects their keys, and the application keeps a second set of keys next to it that nothing protects. In the mental-poker case those keys are exactly what privacy of the user's hand depends on: whoever holds the masking scalars can read every card. Moving them into the wallet moves them behind the wallet's existing consent, storage, and backup guarantees. | ||
|
|
||
| `multiplyPoint` closes the gap with one method and no new cryptographic assumptions. It exposes the group operation the curve already provides, under the key derivation BRC-100 already mandates. | ||
|
|
||
| ## Specification | ||
|
|
||
| ### Method | ||
|
|
||
| A wallet implementing this specification exposes the following method, in keeping with the | ||
| [BRC-100](./0100.md) interface conventions. It is declared **optional** on the interface, for the | ||
| reason given in [Optionality](#optionality) — that requirement is normative and is not a matter of | ||
| implementation convenience: | ||
|
|
||
| ```ts | ||
| /** | ||
| * Multiplies a caller-supplied secp256k1 point by a derived private key, returning the | ||
| * resulting point. The private key is never revealed. | ||
| * | ||
| * @param {Object} args - Contains the point, the protocol and key IDs for derivation, and options. | ||
| * @param {PubKeyHex} args.point - The point to multiply, as a compressed DER-encoded secp256k1 point. | ||
| * @param {[0 | 1 | 2, ProtocolString5To400Characters]} args.protocolID - BRC-43 security level and protocol ID. | ||
| * @param {KeyIDStringUnder800Characters} args.keyID - BRC-43 key ID. | ||
| * @param {PubKeyHex | 'self' | 'anyone'} [args.counterparty] - Counterparty for derivation. Default 'self'. | ||
| * @param {BooleanDefaultFalse} [args.invert] - Multiply by the modular inverse of the derived key instead. | ||
| * @param {BooleanDefaultFalse} [args.privileged] - Whether this is a privileged request. | ||
| * @param {DescriptionString5to50Characters} [args.privilegedReason] - Reason for privileged access. | ||
| * @param {BooleanDefaultTrue} [args.seekPermission] - Whether to seek user permission if required. | ||
| * @param {OriginatorDomainNameString} [originator] - FQDN of the originating application. | ||
| * @returns {Promise<Object>} Resolves to the resulting point, or an error response. | ||
| */ | ||
| multiplyPoint?: ( | ||
| args: { | ||
| point: PubKeyHex | ||
| protocolID: [0 | 1 | 2, ProtocolString5To400Characters] | ||
| keyID: KeyIDStringUnder800Characters | ||
| counterparty?: PubKeyHex | 'self' | 'anyone' | ||
| invert?: BooleanDefaultFalse | ||
| privileged?: BooleanDefaultFalse | ||
| privilegedReason?: DescriptionString5to50Characters | ||
| seekPermission?: BooleanDefaultTrue | ||
| }, | ||
| originator?: OriginatorDomainNameString | ||
| ) => Promise<{ point: PubKeyHex }> | ||
| ``` | ||
|
|
||
| No companion "derive this point" method is specified, because `getPublicKey` already provides it: with the same `protocolID`, `keyID`, and `counterparty`, `getPublicKey` returns `d·G`, which is the derived point for the generator. | ||
|
|
||
| ### Optionality | ||
|
|
||
| `multiplyPoint` **MUST** be declared as an optional member of the wallet interface, and an | ||
| application **MUST** feature-detect it rather than assume its presence: | ||
|
|
||
| ```ts | ||
| if (typeof wallet.multiplyPoint === 'function') { | ||
| // wallet-native masking is available | ||
| } else { | ||
| // degrade: the protocol cannot use a wallet-held masking key | ||
| } | ||
| ``` | ||
|
|
||
| A wallet that does not implement the method is fully conformant with [BRC-100](./0100.md); it is | ||
| simply not usable for the protocols this specification enables. | ||
|
|
||
| This is not a stylistic preference, and the requirement was discovered rather than assumed. BRC-100 | ||
| is specified as an *unchanging* interface, which has a consequence for every later addition: a | ||
| method declared mandatory retroactively invalidates every wallet and every substrate already | ||
| shipped against the interface. When the reference implementation first declared `multiplyPoint` | ||
| required, the TypeScript compiler rejected **23 call sites** across the SDK — `WalletClient`, | ||
| `HTTPWalletJSON`, `WalletWireTransceiver`, `window.CWI`, `XDM` and `ReactNativeWebView`, together | ||
| with the key-value store, registry and identity clients — none of which have any reason to perform | ||
| elliptic curve point multiplication. | ||
|
|
||
| Any future BRC adding a method to the BRC-100 interface faces the same constraint. Optionality plus | ||
| feature detection is the only way to extend an interface whose central promise is that it does not | ||
| change. | ||
|
|
||
| ### Key derivation | ||
|
|
||
| 1. The private key used MUST be derived per [BRC-42](../key-derivation/0042.md) and [BRC-43](../key-derivation/0043.md) from `protocolID`, `keyID`, and `counterparty`, using the same derivation `getPublicKey` uses for the same arguments. | ||
| 2. Wallets MUST NOT use the user's identity key, any change or spending key, or any [BRC-44](../key-derivation/0044.md) internal protocol key. | ||
| 3. Wallets MUST reject protocol IDs reserved by [BRC-98](./0098.md) unless they support the named scheme, as for any other BRC-43 operation. | ||
|
|
||
| Point 2 is the load-bearing requirement, not a stylistic preference. See [Security](#security). | ||
|
|
||
| ### Input validation | ||
|
|
||
| Given a supplied point, a conforming wallet MUST reject the request unless all of the following hold. Each check corresponds to a real attack; none may be skipped. | ||
|
|
||
| 1. The encoding is a valid compressed DER secp256k1 point (33 bytes, leading byte `0x02` or `0x03`). | ||
| 2. Both decoded affine coordinates are canonical field elements — each in the range `[0, p)`, where `p` is the secp256k1 field prime. | ||
| 3. The decoded point satisfies the curve equation `y² = x³ + 7 (mod p)`. | ||
| 4. The point is not the identity (point at infinity). | ||
|
|
||
| Check 2 is **not** implied by check 3, and omitting it is the most likely way to build a | ||
| non-conforming implementation. | ||
|
|
||
| This is verified rather than hypothesised. In both reference implementations, the compressed point | ||
| `02` followed by thirty-two `0xff` bytes — an x-coordinate numerically greater than the field prime — | ||
| is accepted by the library's point parser, silently reduced modulo `p` (to `0x1000003d0`), and then | ||
| reported as on-curve: | ||
|
|
||
| | Library | Parser | Result of on-curve check | | ||
| | --- | --- | --- | | ||
| | `@bsv/sdk` (TypeScript) | `PublicKey.fromString` | `validate()` returns `true` | | ||
| | `go-sdk` (Go) | `ec.PublicKeyFromString` | `IsOnCurve` returns `true` | | ||
|
|
||
| Two independently written libraries share the behaviour, so an implementer following only the curve | ||
| equation will accept a point that was never validly encoded. That is the entry point for the | ||
| invalid-curve attack described under [Security](#security). Both reference implementations were | ||
| first written with this defect and a test caught it in each. | ||
|
|
||
| Because the parser is what performs the reduction, a conforming implementation **MUST** perform the | ||
| range check on the encoded coordinate *before* handing it to the parser. Validating the parsed | ||
| point is too late: by then the out-of-range value has already become a different, valid point. | ||
|
|
||
| ### Result | ||
|
|
||
| 1. On success the wallet returns the resulting point, compressed DER-encoded. | ||
| 2. When `invert` is false, the result is `d·P`, where `d` is the derived key and `P` the supplied point. | ||
| 3. When `invert` is true, the result is `d⁻¹·P`, the inverse taken modulo the curve order `n`. | ||
| 4. `multiplyPoint` with `invert: true` applied to the output of `multiplyPoint` with `invert: false` under the same derivation arguments MUST return the original point. | ||
| 5. If the operation would produce the identity, the wallet MUST return an error rather than an encoding of the identity. | ||
|
|
||
| ### Permissions | ||
|
|
||
| 1. The operation is subject to the same permission and consent machinery as any other BRC-100 protocol operation, including [BRC-73](./0073.md) grouped permissions and [BRC-116](./0116.md) permission lifecycle behavior. | ||
| 2. A wallet MAY treat a granted permission for a protocol as covering repeated calls under that protocol. Protocols of this kind are inherently high-volume: dealing a 52-card deck among 6 players is several hundred point multiplications, and prompting per operation would make the method unusable. | ||
| 3. Because the derived key is protocol-scoped, a permission grant for one protocol conveys no ability to operate under another. | ||
|
|
||
| ## Security | ||
|
|
||
| ### This is not a signing oracle | ||
|
|
||
| The obvious objection to "multiply this arbitrary point by my key" is that it looks like an oracle an attacker can query to recover the key. It is not, for two independent reasons. | ||
|
|
||
| **Recovering the scalar from the output is the elliptic curve discrete logarithm problem.** Given `P` and `d·P`, finding `d` is exactly the assumption secp256k1 rests on. An attacker who could do this could equally take any public key and recover its private key. Choosing `P` adaptively does not help: for any chosen `P = k·G` the response is `k·(d·G)`, which the attacker could have computed from `getPublicKey`'s output alone. Chosen-point queries therefore yield nothing that public information does not already yield. | ||
|
|
||
| **There is no signature equation to attack.** ECDSA is fragile under partial-information leakage because the same private key appears in `s = k⁻¹(z + r·d)`, so biased or leaked nonces yield lattice attacks recovering `d`. A key reachable only through `multiplyPoint` never signs. There is no `k`, no `s`, and no equation relating the key to a message. The lattice attacks that make signing oracles dangerous have no analogue here. | ||
|
|
||
| ### The real risk is key reuse, and it is why derivation is mandatory | ||
|
|
||
| The genuine danger is not the operation but the key it is performed with. Note that for a point `Q` belonging to a counterparty, `d·Q` **is** the ECDH shared secret between `d` and `Q`. | ||
|
|
||
| If a wallet implemented `multiplyPoint` over a key used for anything else, the method would hand any caller the shared secret for that key — breaking [BRC-2](./0002.md) encryption to that counterparty, and any HMAC or key-linkage guarantee derived from it. Implemented over the identity key, it would compromise the user's identity-level ECDH secrets wholesale. | ||
|
|
||
| Mandatory BRC-42/43 derivation is precisely what prevents this. A key that exists only under a protocol-scoped derivation, and is never used to sign or to encrypt, has no other security property to lose. Its only capability is the one the application asked for. This is why point 2 under [Key derivation](#key-derivation) is a MUST rather than a SHOULD, and why no `identityKey` option is offered by analogy with `getPublicKey`. | ||
|
|
||
| ### Key linkage | ||
|
|
||
| `multiplyPoint` reveals no more about key relationships than `getPublicKey` already does. Deliberate disclosure of key linkage remains the province of `revealCounterpartyKeyLinkage` and `revealSpecificKeyLinkage`, protected as described in [BRC-72](../key-derivation/0072.md). A wallet MUST NOT treat a `multiplyPoint` grant as authority to reveal linkage. | ||
|
|
||
| ### Invalid-curve attacks | ||
|
|
||
| The validation rules above are the defense against the standard invalid-curve attack, in which a point on a different curve — one with smooth order — is submitted so that the response leaks the key modulo small factors. Rejecting non-canonical encodings, off-curve points, and the identity closes it. This is why those rules are normative requirements rather than implementation advice. | ||
|
|
||
| ## Implementations | ||
|
|
||
| Two reference implementations exist. | ||
|
|
||
| **TypeScript**, in the BSV Association SDK, proposed at | ||
| https://github.com/bsv-blockchain/ts-stack/pull/487 — implemented in `ProtoWallet` using the | ||
| `KeyDeriver`, `Point.mul` and `BigNumber.invm` primitives the package already provides. Verified | ||
| against that repository's own tooling: `tsc -b` clean, `oxlint --deny-warnings` clean, and the full | ||
| SDK suite green at 157 suites and 5924 tests. | ||
|
|
||
| **Go**, at | ||
| https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points | ||
|
|
||
| It was written against `go-sdk` curve primitives as part of a non-custodial poker application, where a dealerless deal is the motivating use case. The tests assert that masks applied by independent wallets commute, that `invert` recovers the original point, that a three-way mask can be stripped in any order, that distinct protocol and key IDs yield independent derivations, that two different wallets never derive the same protocol key, and that a whole-deck mask with a selective unmask leaves the other positions unreadable. | ||
|
|
||
| The validation rules in this specification are drawn from that work rather than asserted in advance. The first draft validated points with an on-curve check alone, and a test submitting a compressed point whose x-coordinate is thirty-two `0xff` bytes — a value greater than the secp256k1 field prime — was accepted: the parser reduced the coordinate silently and the on-curve test then passed. The canonical-encoding requirement exists because that hole is easy to ship. | ||
|
|
||
| No wallet implements `multiplyPoint` at the time of writing. Applications needing commutative masking today must either hold keys outside the wallet or degrade to a trusted dealer; this proposal exists to remove that choice. | ||
|
|
||
| ## References | ||
|
|
||
| - [BRC-2: Data Encryption and Decryption](./0002.md) | ||
| - [BRC-42: BSV Key Derivation Scheme (BKDS)](../key-derivation/0042.md) | ||
| - [BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties](../key-derivation/0043.md) | ||
| - [BRC-44: Admin-reserved and Prohibited Key Derivation Protocols](../key-derivation/0044.md) | ||
| - [BRC-72: Protecting BRC-69 Key Linkage Information in Transit](../key-derivation/0072.md) | ||
| - [BRC-73: Group Permissions for App Access](./0073.md) | ||
| - [BRC-98: P Protocols: Allowing Future Wallet Protocol Permission Schemes](./0098.md) | ||
| - [BRC-100: Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface](./0100.md) | ||
| - [BRC-116: Wallet Permissions and Counterparty Trust](./0116.md) | ||
| - <a name="footnote-1">1</a>: Barnett, A. and Smart, N. (2003). Mental Poker Revisited. Cryptography and Coding, LNCS 2898, pp. 370–383. | ||
| - <a name="footnote-2">2</a>: Biehl, I., Meyer, B. and Müller, V. (2000). Differential Fault Attacks on Elliptic Curve Cryptosystems. CRYPTO 2000, LNCS 1880, pp. 131–146. (Invalid-curve attacks.) |
There was a problem hiding this comment.
| # BRC-229: Wallet-Native Elliptic Curve Point Multiplication | |
| Connor Murray (connor.murray@bsvassociation.org) | |
| ## Abstract | |
| This proposal adds a single method, `multiplyPoint`, to the [BRC-100](./0100.md) wallet interface. It multiplies a caller-supplied secp256k1 point by a [BRC-42](../key-derivation/0042.md)/[BRC-43](../key-derivation/0043.md) derived private key and returns the resulting point, without ever disclosing the key. An `invert` flag multiplies by the modular inverse of that key, so a transformation applied by the wallet can be removed by the wallet. | |
| This is the smallest addition that lets commutative-masking protocols — mental poker, verifiable shuffles, oblivious transfer, threshold key exchange — run inside a BRC-100 wallet instead of requiring an application to hold raw private keys of its own. | |
| ## Motivation | |
| BRC-100 gives an application a complete vocabulary for asking a wallet to act on the user's keys: sign a transaction, produce a signature, encrypt, HMAC, derive a public key. Every one of those operations is defined over data the wallet chooses to interpret. None of them expose the underlying group operation. | |
| That gap has a concrete consequence. A class of multi-party protocols is built on the commutativity of scalar multiplication: | |
| ``` | |
| a·(b·P) == b·(a·P) | |
| ``` | |
| Barnett–Smart mental poker is the canonical example. Each participant masks every card with a secret scalar; because masking commutes, the participants can apply their masks in any order and later strip them in any order. No participant ever learns a card another participant was dealt, and no dealer exists to trust. This is the standard construction for trustless card games, and the same primitive underlies verifiable shuffles and several oblivious-transfer constructions. | |
| A BRC-100 wallet cannot participate in any of it. `getPublicKey` returns `d·G` for a derived key — the generator multiplied by the scalar — but there is no way to ask for `d·P` for an arbitrary point `P`. So an application that needs commutative masking must generate and hold its own secp256k1 keys outside the wallet, in application storage, with application-grade key hygiene. | |
| The result is the outcome BRC-100 exists to prevent. The user has a wallet that protects their keys, and the application keeps a second set of keys next to it that nothing protects. In the mental-poker case those keys are exactly what privacy of the user's hand depends on: whoever holds the masking scalars can read every card. Moving them into the wallet moves them behind the wallet's existing consent, storage, and backup guarantees. | |
| `multiplyPoint` closes the gap with one method and no new cryptographic assumptions. It exposes the group operation the curve already provides, under the key derivation BRC-100 already mandates. | |
| ## Specification | |
| ### Method | |
| A wallet implementing this specification exposes the following method, in keeping with the | |
| [BRC-100](./0100.md) interface conventions. It is declared **optional** on the interface, for the | |
| reason given in [Optionality](#optionality) — that requirement is normative and is not a matter of | |
| implementation convenience: | |
| ```ts | |
| /** | |
| * Multiplies a caller-supplied secp256k1 point by a derived private key, returning the | |
| * resulting point. The private key is never revealed. | |
| * | |
| * @param {Object} args - Contains the point, the protocol and key IDs for derivation, and options. | |
| * @param {PubKeyHex} args.point - The point to multiply, as a compressed DER-encoded secp256k1 point. | |
| * @param {[0 | 1 | 2, ProtocolString5To400Characters]} args.protocolID - BRC-43 security level and protocol ID. | |
| * @param {KeyIDStringUnder800Characters} args.keyID - BRC-43 key ID. | |
| * @param {PubKeyHex | 'self' | 'anyone'} [args.counterparty] - Counterparty for derivation. Default 'self'. | |
| * @param {BooleanDefaultFalse} [args.invert] - Multiply by the modular inverse of the derived key instead. | |
| * @param {BooleanDefaultFalse} [args.privileged] - Whether this is a privileged request. | |
| * @param {DescriptionString5to50Characters} [args.privilegedReason] - Reason for privileged access. | |
| * @param {BooleanDefaultTrue} [args.seekPermission] - Whether to seek user permission if required. | |
| * @param {OriginatorDomainNameString} [originator] - FQDN of the originating application. | |
| * @returns {Promise<Object>} Resolves to the resulting point, or an error response. | |
| */ | |
| multiplyPoint?: ( | |
| args: { | |
| point: PubKeyHex | |
| protocolID: [0 | 1 | 2, ProtocolString5To400Characters] | |
| keyID: KeyIDStringUnder800Characters | |
| counterparty?: PubKeyHex | 'self' | 'anyone' | |
| invert?: BooleanDefaultFalse | |
| privileged?: BooleanDefaultFalse | |
| privilegedReason?: DescriptionString5to50Characters | |
| seekPermission?: BooleanDefaultTrue | |
| }, | |
| originator?: OriginatorDomainNameString | |
| ) => Promise<{ point: PubKeyHex }> | |
| ``` | |
| No companion "derive this point" method is specified, because `getPublicKey` already provides it: with the same `protocolID`, `keyID`, and `counterparty`, `getPublicKey` returns `d·G`, which is the derived point for the generator. | |
| ### Optionality | |
| `multiplyPoint` **MUST** be declared as an optional member of the wallet interface, and an | |
| application **MUST** feature-detect it rather than assume its presence: | |
| ```ts | |
| if (typeof wallet.multiplyPoint === 'function') { | |
| // wallet-native masking is available | |
| } else { | |
| // degrade: the protocol cannot use a wallet-held masking key | |
| } | |
| ``` | |
| A wallet that does not implement the method is fully conformant with [BRC-100](./0100.md); it is | |
| simply not usable for the protocols this specification enables. | |
| This is not a stylistic preference, and the requirement was discovered rather than assumed. BRC-100 | |
| is specified as an *unchanging* interface, which has a consequence for every later addition: a | |
| method declared mandatory retroactively invalidates every wallet and every substrate already | |
| shipped against the interface. When the reference implementation first declared `multiplyPoint` | |
| required, the TypeScript compiler rejected **23 call sites** across the SDK — `WalletClient`, | |
| `HTTPWalletJSON`, `WalletWireTransceiver`, `window.CWI`, `XDM` and `ReactNativeWebView`, together | |
| with the key-value store, registry and identity clients — none of which have any reason to perform | |
| elliptic curve point multiplication. | |
| Any future BRC adding a method to the BRC-100 interface faces the same constraint. Optionality plus | |
| feature detection is the only way to extend an interface whose central promise is that it does not | |
| change. | |
| ### Key derivation | |
| 1. The private key used MUST be derived per [BRC-42](../key-derivation/0042.md) and [BRC-43](../key-derivation/0043.md) from `protocolID`, `keyID`, and `counterparty`, using the same derivation `getPublicKey` uses for the same arguments. | |
| 2. Wallets MUST NOT use the user's identity key, any change or spending key, or any [BRC-44](../key-derivation/0044.md) internal protocol key. | |
| 3. Wallets MUST reject protocol IDs reserved by [BRC-98](./0098.md) unless they support the named scheme, as for any other BRC-43 operation. | |
| Point 2 is the load-bearing requirement, not a stylistic preference. See [Security](#security). | |
| ### Input validation | |
| Given a supplied point, a conforming wallet MUST reject the request unless all of the following hold. Each check corresponds to a real attack; none may be skipped. | |
| 1. The encoding is a valid compressed DER secp256k1 point (33 bytes, leading byte `0x02` or `0x03`). | |
| 2. Both decoded affine coordinates are canonical field elements — each in the range `[0, p)`, where `p` is the secp256k1 field prime. | |
| 3. The decoded point satisfies the curve equation `y² = x³ + 7 (mod p)`. | |
| 4. The point is not the identity (point at infinity). | |
| Check 2 is **not** implied by check 3, and omitting it is the most likely way to build a | |
| non-conforming implementation. | |
| This is verified rather than hypothesised. In both reference implementations, the compressed point | |
| `02` followed by thirty-two `0xff` bytes — an x-coordinate numerically greater than the field prime — | |
| is accepted by the library's point parser, silently reduced modulo `p` (to `0x1000003d0`), and then | |
| reported as on-curve: | |
| | Library | Parser | Result of on-curve check | | |
| | --- | --- | --- | | |
| | `@bsv/sdk` (TypeScript) | `PublicKey.fromString` | `validate()` returns `true` | | |
| | `go-sdk` (Go) | `ec.PublicKeyFromString` | `IsOnCurve` returns `true` | | |
| Two independently written libraries share the behaviour, so an implementer following only the curve | |
| equation will accept a point that was never validly encoded. That is the entry point for the | |
| invalid-curve attack described under [Security](#security). Both reference implementations were | |
| first written with this defect and a test caught it in each. | |
| Because the parser is what performs the reduction, a conforming implementation **MUST** perform the | |
| range check on the encoded coordinate *before* handing it to the parser. Validating the parsed | |
| point is too late: by then the out-of-range value has already become a different, valid point. | |
| ### Result | |
| 1. On success the wallet returns the resulting point, compressed DER-encoded. | |
| 2. When `invert` is false, the result is `d·P`, where `d` is the derived key and `P` the supplied point. | |
| 3. When `invert` is true, the result is `d⁻¹·P`, the inverse taken modulo the curve order `n`. | |
| 4. `multiplyPoint` with `invert: true` applied to the output of `multiplyPoint` with `invert: false` under the same derivation arguments MUST return the original point. | |
| 5. If the operation would produce the identity, the wallet MUST return an error rather than an encoding of the identity. | |
| ### Permissions | |
| 1. The operation is subject to the same permission and consent machinery as any other BRC-100 protocol operation, including [BRC-73](./0073.md) grouped permissions and [BRC-116](./0116.md) permission lifecycle behavior. | |
| 2. A wallet MAY treat a granted permission for a protocol as covering repeated calls under that protocol. Protocols of this kind are inherently high-volume: dealing a 52-card deck among 6 players is several hundred point multiplications, and prompting per operation would make the method unusable. | |
| 3. Because the derived key is protocol-scoped, a permission grant for one protocol conveys no ability to operate under another. | |
| ## Security | |
| ### This is not a signing oracle | |
| The obvious objection to "multiply this arbitrary point by my key" is that it looks like an oracle an attacker can query to recover the key. It is not, for two independent reasons. | |
| **Recovering the scalar from the output is the elliptic curve discrete logarithm problem.** Given `P` and `d·P`, finding `d` is exactly the assumption secp256k1 rests on. An attacker who could do this could equally take any public key and recover its private key. Choosing `P` adaptively does not help: for any chosen `P = k·G` the response is `k·(d·G)`, which the attacker could have computed from `getPublicKey`'s output alone. Chosen-point queries therefore yield nothing that public information does not already yield. | |
| **There is no signature equation to attack.** ECDSA is fragile under partial-information leakage because the same private key appears in `s = k⁻¹(z + r·d)`, so biased or leaked nonces yield lattice attacks recovering `d`. A key reachable only through `multiplyPoint` never signs. There is no `k`, no `s`, and no equation relating the key to a message. The lattice attacks that make signing oracles dangerous have no analogue here. | |
| ### The real risk is key reuse, and it is why derivation is mandatory | |
| The genuine danger is not the operation but the key it is performed with. Note that for a point `Q` belonging to a counterparty, `d·Q` **is** the ECDH shared secret between `d` and `Q`. | |
| If a wallet implemented `multiplyPoint` over a key used for anything else, the method would hand any caller the shared secret for that key — breaking [BRC-2](./0002.md) encryption to that counterparty, and any HMAC or key-linkage guarantee derived from it. Implemented over the identity key, it would compromise the user's identity-level ECDH secrets wholesale. | |
| Mandatory BRC-42/43 derivation is precisely what prevents this. A key that exists only under a protocol-scoped derivation, and is never used to sign or to encrypt, has no other security property to lose. Its only capability is the one the application asked for. This is why point 2 under [Key derivation](#key-derivation) is a MUST rather than a SHOULD, and why no `identityKey` option is offered by analogy with `getPublicKey`. | |
| ### Key linkage | |
| `multiplyPoint` reveals no more about key relationships than `getPublicKey` already does. Deliberate disclosure of key linkage remains the province of `revealCounterpartyKeyLinkage` and `revealSpecificKeyLinkage`, protected as described in [BRC-72](../key-derivation/0072.md). A wallet MUST NOT treat a `multiplyPoint` grant as authority to reveal linkage. | |
| ### Invalid-curve attacks | |
| The validation rules above are the defense against the standard invalid-curve attack, in which a point on a different curve — one with smooth order — is submitted so that the response leaks the key modulo small factors. Rejecting non-canonical encodings, off-curve points, and the identity closes it. This is why those rules are normative requirements rather than implementation advice. | |
| ## Implementations | |
| Two reference implementations exist. | |
| **TypeScript**, in the BSV Association SDK, proposed at | |
| https://github.com/bsv-blockchain/ts-stack/pull/487 — implemented in `ProtoWallet` using the | |
| `KeyDeriver`, `Point.mul` and `BigNumber.invm` primitives the package already provides. Verified | |
| against that repository's own tooling: `tsc -b` clean, `oxlint --deny-warnings` clean, and the full | |
| SDK suite green at 157 suites and 5924 tests. | |
| **Go**, at | |
| https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points | |
| It was written against `go-sdk` curve primitives as part of a non-custodial poker application, where a dealerless deal is the motivating use case. The tests assert that masks applied by independent wallets commute, that `invert` recovers the original point, that a three-way mask can be stripped in any order, that distinct protocol and key IDs yield independent derivations, that two different wallets never derive the same protocol key, and that a whole-deck mask with a selective unmask leaves the other positions unreadable. | |
| The validation rules in this specification are drawn from that work rather than asserted in advance. The first draft validated points with an on-curve check alone, and a test submitting a compressed point whose x-coordinate is thirty-two `0xff` bytes — a value greater than the secp256k1 field prime — was accepted: the parser reduced the coordinate silently and the on-curve test then passed. The canonical-encoding requirement exists because that hole is easy to ship. | |
| No wallet implements `multiplyPoint` at the time of writing. Applications needing commutative masking today must either hold keys outside the wallet or degrade to a trusted dealer; this proposal exists to remove that choice. | |
| ## References | |
| - [BRC-2: Data Encryption and Decryption](./0002.md) | |
| - [BRC-42: BSV Key Derivation Scheme (BKDS)](../key-derivation/0042.md) | |
| - [BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties](../key-derivation/0043.md) | |
| - [BRC-44: Admin-reserved and Prohibited Key Derivation Protocols](../key-derivation/0044.md) | |
| - [BRC-72: Protecting BRC-69 Key Linkage Information in Transit](../key-derivation/0072.md) | |
| - [BRC-73: Group Permissions for App Access](./0073.md) | |
| - [BRC-98: P Protocols: Allowing Future Wallet Protocol Permission Schemes](./0098.md) | |
| - [BRC-100: Unified, Vendor-Neutral, Unchanging, and Open BSV Blockchain Standard Wallet-to-Application Interface](./0100.md) | |
| - [BRC-116: Wallet Permissions and Counterparty Trust](./0116.md) | |
| - <a name="footnote-1">1</a>: Barnett, A. and Smart, N. (2003). Mental Poker Revisited. Cryptography and Coding, LNCS 2898, pp. 370–383. | |
| - <a name="footnote-2">2</a>: Biehl, I., Meyer, B. and Müller, V. (2000). Differential Fault Attacks on Elliptic Curve Cryptosystems. CRYPTO 2000, LNCS 1880, pp. 131–146. (Invalid-curve attacks.) | |
| # BRC-229: Wallet-Native Elliptic Curve Point Multiplication as a BRC-98 Module | |
| Connor Murray (connor.murray@bsvassociation.org) | |
| ## Abstract | |
| This proposal defines `ecpm`, a [BRC-98](./0098.md) permission-module scheme for applying a wallet-derived secp256k1 scalar to an arbitrary caller-supplied curve point, or removing that scalar by applying its modular inverse. | |
| The operation is carried over the existing [BRC-100](./0100.md) `getPublicKey` method. Inside the reserved `p ecpm` namespace, a supporting wallet interprets the method semantically as elliptic-curve point multiplication and returns the normal `{ publicKey }` result. No method, call code, Wallet Wire message, or optional member is added to BRC-100. | |
| The scheme enables commutative-masking protocols such as mental poker and verifiable shuffles while keeping the derived scalar inside the wallet. Wallets that do not install the module retain BRC-98's required behavior and reject the reserved protocol. | |
| ## Motivation | |
| A class of multi-party protocols depends on the commutativity of scalar multiplication: | |
a·(b·P) = b·(a·P)
Barnett-Smart mental poker is a representative use. Each participant applies a secret scalar to encoded card points, and later removes that scalar in any order. The application needs `d·P` and `d⁻¹·P` for an arbitrary point `P`, but it must not learn `d`.
Ordinary BRC-100 `getPublicKey` does not supply this operation. It performs BRC-42/43 child-key derivation and returns a derived public key. Naming `P` as the BRC-43 counterparty influences the child derivation, but does not replace the generator with `P`. Depending on `forSelf`, the result is the wallet's or counterparty's derived child public key, not the wallet's derived private scalar multiplied by the caller's point.
This distinction matters when `P` is an intermediate masked point whose discrete logarithm is intentionally unknown. From `d·G` and `P`, an application cannot compute `d·P` without solving a discrete logarithm or already knowing the scalar of `P`.
Existing key-linkage and encryption methods can be composed to emulate some forward-multiplication cases, but that is not an equivalent contract. It couples a point operation to linkage disclosure and encryption semantics, does not provide multiplication by `d⁻¹`, and grants permissions for capabilities the application did not mean to request.
The missing behavior therefore cannot be obtained from pure BRC-43 naming alone. It requires a wallet-side semantic operation, but it does not require changing BRC-100.
## Relationship to BRC-98
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 `ecpm` scheme uses that reserved dispatch point to specialize the meaning of an existing BRC-100 method. This proposal does not amend BRC-98. A clarification in discussion of BRC-98 may record that semantic specialization of an existing method inside a supported module namespace is an intended use of the module system; a separate amendment to BRC-98 is not required.
The semantic boundary is safe for compatibility:
1. Outside `p ecpm`, `getPublicKey` retains its ordinary BRC-100 meaning.
2. Inside `p ecpm`, a wallet either implements this scheme or rejects the request as BRC-98 already requires.
3. 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:
ecpm
A conforming implementation MUST accept this scheme only through the existing BRC-100 `getPublicKey` method. It MUST reject every other BRC-100 method requested under `p ecpm`. This prevents the ECPM scalar from being reused for signing, HMAC, encryption, or another cryptographic purpose.
The protocol-name component of `GetPublicKeyArgs.protocolID` has this grammar:
p ecpm
where:
- `operation` is exactly `apply` or `remove`;
- `pointHex` is a lowercase, 66-character compressed secp256k1 public-key encoding;
- `logicalProtocolID` is the application protocol whose ECPM key universe is being requested.
The outer BRC-100 call remains:
```ts
wallet.getPublicKey(
{
protocolID: [
securityLevel,
`p ecpm ${operation} ${pointHex} ${logicalProtocolID}`
],
keyID,
counterparty,
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. identityKey: true is prohibited. forSelf MUST be absent or false; it does not alter ECPM semantics.
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 273 characters; and
- not end with
protocol.
The 273-character ceiling ensures that the canonical derivation protocol p ecpm <logicalProtocolID> remains within BRC-43's 280-character protocol-ID limit.
The complete outer p ecpm protocol string also MUST fit the active BRC-100 protocol-string limit. A key ID MUST satisfy the active BRC-100 getPublicKey key-ID limit.
Canonical scalar derivation
Let:
sbe the security level in the outerprotocolIDtuple;LbelogicalProtocolID;Kbe the separately suppliedkeyID; andCbe the separately suppliedcounterparty, defaulting toself.
The module MUST derive a nonzero private scalar d using BRC-42/43 with:
protocolID = [s, "p ecpm " + L]
keyID = K
counterparty = C
The point and operation MUST NOT form part of this derivation identity.
This omission is load-bearing. If the point were part of the invoice, every point would select a different scalar. If the operation were part of the invoice, remove would select a different scalar from apply. In either case round-trip removal and commutativity would fail.
The p ecpm prefix in the canonical derivation protocol isolates ECPM scalars from ordinary application keys using the same logical protocol and key ID.
Operation
After validating the point as P:
applyreturnsd·P;removereturnsd⁻¹·P, where the inverse is computed modulo the secp256k1 group ordern.
The successful result MUST use the ordinary getPublicKey result shape:
{ publicKey: PubKeyHex }The returned value MUST be the lowercase compressed encoding of the resulting point.
For identical security level, logical protocol ID, key ID, counterparty, and root-key selection:
remove(apply(P)) = P
Implementations MUST reject, rather than encode, a result at infinity.
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:
privilegedReasonMUST be present and satisfy the existing BRC-100 description constraint.- The wallet MUST obtain explicit authorization even at security level 0.
- The wallet MUST derive
dfrom its privileged key material using the same canonical ECPM derivation tuple. - The privileged root or key-derivation capability MUST NOT be exposed to the application.
- 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, and SHOULD release that access according to its existing privileged-key policy.
Permissions
The module applies BRC-43 permission semantics to the logical ECPM 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.
A wallet MAY cache a successful grant according to its normal permission lifecycle. The point, operation, and key ID SHOULD be displayed or made available to the authorization UI, but they do not change the BRC-43 protocol-level grant scope.
If permission is required and no applicable grant exists:
seekPermission: falseMUST 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.
Point validation
Before multiplication, a conforming implementation MUST reject the input unless all of these conditions hold:
- The encoding is exactly 33 bytes represented by 66 lowercase hexadecimal characters.
- The first byte is
02or03. - The encoded x-coordinate is less than the secp256k1 field prime
p. - The encoding decodes to a point satisfying
y² = x³ + 7 (mod p). - The point is finite.
The x-coordinate range check MUST occur before a parser that reduces coordinates modulo p. Some curve parsers accept 02 followed by 32 ff bytes, reduce the x-coordinate, and then report the resulting different point as valid.
The same canonical validation MUST be applied when counterparty is supplied as a public key. The special values self and anyone remain valid.
Errors
A conforming wallet MUST fail without performing multiplication when:
- the installed method is not
getPublicKey; - the protocol grammar or any field constraint is invalid;
- identity-key or
forSelf: truebehavior is requested; - the point or public-key counterparty is invalid;
- authorization is unavailable or denied;
- privileged derivation is requested but unavailable; or
- the operation would return infinity.
Errors SHOULD identify the unsupported scheme, invalid field, or denied capability without revealing key material.
TypeScript reference module
The TypeScript reference implementation is proposed in bsv-blockchain/ts-stack#488 as the installable @bsv/ecpm-permission-module package.
It extends the Wallet Toolbox permission-module interface with an optional semantic handler:
handleRequest?: (
request: { method: string; args: object; originator: string },
next: (args: object) => Promise<unknown>
) => Promise<unknown>A semantic handler may return a conforming BRC-100 result directly, or invoke next at most once to use the underlying wallet method. Existing onRequest and onResponse transformation modules remain source-compatible.
The ECPM module returns the result directly and never forwards its request to ordinary getPublicKey; forwarding would derive d·G or a BRC-42 child public key, not d·P.
A wallet host installs it alongside other BRC-98 modules under the ecpm scheme and supplies:
- its ordinary BRC-42/43 key deriver;
- its trusted authorization callback; and
- optionally, a privileged-key-deriver provider.
The key derivers and derived scalars remain inside trusted wallet/module code and are never included in the BRC-100 response.
Security
Key isolation
For a counterparty point Q, d·Q is an ECDH shared secret. Reusing an identity, spending, signing, HMAC, or encryption key for ECPM could therefore disclose a capability belonging to another protocol.
For this reason, the canonical p ecpm <logicalProtocolID> derivation namespace and the method restriction are mandatory. A wallet MUST NOT substitute its identity key, spending key, or a key from an ordinary non-ECPM protocol.
Chosen-point requests
For a valid prime-order secp256k1 point P, observing P and d·P does not reveal d without solving the elliptic-curve discrete logarithm problem. An adaptively selected valid point does not weaken this assumption in the prime-order group.
That argument depends on strict validation. Accepting a point on another curve or a non-canonical encoding can create small-subgroup or invalid-curve attacks that leak information about d. Implementations must validate the encoded point before multiplication.
Permission and denial-of-service considerations
Mental-poker and shuffle protocols can require hundreds of operations, so prompting once per point is impractical. Protocol-scoped grants allow the intended throughput. Wallets SHOULD still bound request concurrency and resource use, and SHOULD make the logical protocol, originator, operation, key ID, counterparty, point, and privileged reason available to trusted policy code.
Capability containment
The public application interface exposes only getPublicKey and receives only a compressed public point. The semantic module is trusted wallet code: it may use internal key-derivation capabilities, but it MUST NOT return those capabilities, a private scalar, or privileged root material.
Why pure BRC-43 is insufficient
BRC-43 can name the counterparty and thereby determine which shared derivation universe is used. It cannot name a replacement generator for the requested group operation.
If the arbitrary point is supplied as counterparty, ordinary getPublicKey performs BRC-42 child derivation relative to that counterparty. It does not compute the wallet's derived private scalar times that point. If getPublicKey returns d·G, combining it with an arbitrary P still does not yield d·P unless the application knows the discrete logarithm of P. Intermediate points in a commutative-masking protocol are constructed specifically so that no one knows that logarithm.
Pure BRC-43 therefore provides the scalar namespace and permission vocabulary used by this proposal, but not the required group operation. BRC-98 supplies the missing semantic dispatch without changing BRC-100.
References
- BRC-2: Data Encryption and Decryption
- BRC-42: BSV Key Derivation Scheme
- BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties
- BRC-44: Admin-reserved and Prohibited Key Derivation Protocols
- BRC-73: Group Permissions for App Access
- BRC-98: P Protocols
- BRC-100: Unified Wallet-to-Application Interface
- BRC-116: Wallet Permissions and Counterparty Trust
Adds one method to the BRC-100 interface:
multiplyPoint, which multiplies a caller-supplied secp256k1 point by a BRC-42/43 derived private key and returns the result, without disclosing the key. Aninvertflag multiplies by the modular inverse, so a mask the wallet applies is a mask the wallet can remove.Why
A class of multi-party protocols rests on the commutativity of scalar multiplication,
a·(b·P) == b·(a·P): Barnett–Smart mental poker, verifiable shuffles, several oblivious-transfer constructions. BRC-100 exposes no way to reach that group operation.getPublicKeygivesd·G, but nothing givesd·Pfor an arbitraryP.So an application that needs commutative masking has to generate and hold its own secp256k1 keys outside the wallet. In the mental-poker case those keys are precisely what the privacy of the user's hand depends on — whoever holds the masking scalars can read every card. The user ends up with a wallet that protects their keys and an application holding a second set that nothing protects, which is the outcome BRC-100 exists to prevent.
No companion
derivePointis proposed.getPublicKeyalready returnsd·Gfor the same derivation arguments, so a second method would be redundant.On the obvious objection
"Multiply this arbitrary point by my key" reads like a signing oracle. The spec argues it is not, on two independent grounds: recovering the scalar from
Pandd·Pis ECDLP, and adaptively chosenP = k·Gonly ever returnsk·(d·G), which the caller could compute fromgetPublicKey's output alone. Separately, ECDSA is fragile under leakage because the same key sits ins = k⁻¹(z + r·d); a key reachable only throughmultiplyPointnever signs, so there is no such equation to attack.The risk that is real gets named plainly: for a counterparty point
Q,d·Qis the ECDH shared secret. Implemented over a spending or identity key, this method would hand any caller that secret and break BRC-2 encryption to that counterparty. Mandatory BRC-42/43 derivation is what prevents it — which is why that rule is a MUST and why noidentityKeyoption is offered by analogy withgetPublicKey.Validation rules come from implementation, not from theory
The normative validation rules include a canonical-encoding check on both coordinates, and the spec states that this is not implied by an on-curve check. That is in there because the reference implementation shipped with exactly that hole: a test submitting a compressed point whose x-coordinate is thirty-two
0xffbytes — greater than the field prime — was accepted, because the parser reduced the coordinate silently and the on-curve test then passed. Easy bug to ship, so the spec makes the check mandatory rather than advisory.Reference implementation
https://github.com/connormurray2/brc100-poker/tree/main/internal/brc/points
Go, with tests asserting the properties the spec depends on: masks from independent wallets commute,
invertrecovers the original point, a three-way mask strips in any order, distinct protocol and key IDs derive independently, two wallets never derive the same protocol key, and a whole-deck mask with a selective unmask leaves the other positions unreadable.Written as part of a non-custodial poker application, where a dealerless deal is the motivating use case. No wallet implements
multiplyPointtoday; applications needing commutative masking must currently either hold keys outside the wallet or fall back to a trusted dealer.Happy to revise the shape, the naming, or the permission semantics — particularly the call-code assignment (29) and whether per-protocol grants covering repeated calls is the right permission model for a method this high-volume.