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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ export class VerifiableCertificate extends Certificate {
* @throws {Error} Throws an error if any of the decryption operations fail, with a message indicating the failure context.
*/
async decryptFields(
verifierWallet: ProtoWallet,
// Typed as the capability actually used rather than the whole ProtoWallet class, so
// that adding a method to ProtoWallet does not narrow what may be passed here. This
// only ever calls decrypt().
verifierWallet: Pick<ProtoWallet, 'decrypt'>,
privileged?: boolean,
privilegedReason?: string,
originator?: OriginatorDomainNameStringUnder250Bytes
Expand Down
106 changes: 106 additions & 0 deletions packages/sdk/src/wallet/ProtoWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
PublicKey,
Point,
PrivateKey,
Curve,
SymmetricKey,
readyAsyncCryptoBackend,
isAsyncCryptoDigest,
Expand All @@ -20,6 +21,8 @@ import {
CreateSignatureArgs,
CreateSignatureResult,
GetPublicKeyArgs,
MultiplyPointArgs,
MultiplyPointResult,
PubKeyHex,
RevealCounterpartyKeyLinkageArgs,
RevealCounterpartyKeyLinkageResult,
Expand Down Expand Up @@ -88,6 +91,56 @@ async function deriveSymmetricKey(
return keyDeriver.deriveSymmetricKey(protocolID, keyID, counterparty)
}

/**
* Parses a compressed DER point, rejecting everything a conforming BRC-229
* implementation must reject.
*
* The canonical-encoding check is NOT redundant with the on-curve check, and omitting it is
* the likely way to build a non-conforming implementation. PublicKey.fromString accepts an
* x-coordinate numerically greater than the field prime, reduces it modulo p without
* signalling anything, and validate() then reports the reduced point as on-curve. Verified
* against this package: '02' + 'ff'*32 parses, reduces to 0x1000003d0, and validates true.
* Accepting it would admit a point that was never validly encoded, which is the entry point
* for invalid-curve attacks.
*/
function parseValidPoint(pointHex: PubKeyHex): Point {
// A wrong type and a wrong format are different faults, so they raise different errors:
// TypeError for the former (which is also what Sonar's S7786 asks for), Error for the latter.
if (typeof pointHex !== 'string') {
throw new TypeError('multiplyPoint: point must be a string')
}
if (!/^0[23][0-9a-fA-F]{64}$/.test(pointHex)) {
throw new Error('multiplyPoint: point must be a 33-byte compressed DER secp256k1 point in hex')
}

const curve = new Curve()
// Reject before parsing, because the parser is what silently reduces the coordinate.
if (new BigNumber(pointHex.slice(2), 16).cmp(curve.p) >= 0) {
throw new Error('multiplyPoint: the x-coordinate is not a canonical field element')
}

let point: Point
try {
point = Point.fromString(pointHex)
} catch {
throw new Error('multiplyPoint: the supplied point could not be decoded')
}

if (point.isInfinity()) {
throw new Error('multiplyPoint: the supplied point is the identity')
}
// Guard y as well. The compressed form derives y, but a decoder that produced a
// non-canonical y would otherwise reach the curve check unexamined.
const y = point.getY()
if (y.cmp(curve.p) >= 0 || y.isNeg()) {
throw new Error('multiplyPoint: the y-coordinate is not a canonical field element')
}
if (!point.validate()) {
throw new Error('multiplyPoint: the supplied point is not on the curve')
}
return point
}

/**
* A ProtoWallet is precursor to a full wallet, capable of performing all foundational cryptographic operations.
* It can derive keys, create signatures, facilitate encryption and HMAC operations, and reveal key linkages.
Expand Down Expand Up @@ -130,6 +183,59 @@ export class ProtoWallet {
}
}

/**
* Multiplies a caller-supplied secp256k1 point by a BRC-42/43 derived private key,
* returning the resulting point. The private key is never revealed.
*
* This is the BRC-229 primitive. It exists so commutative-masking protocols
* (Barnett-Smart mental poker, verifiable shuffles, oblivious transfer) can run against a
* wallet-held key instead of requiring the application to generate and store secp256k1
* keys of its own.
*
* The key is always derived from protocolID/keyID/counterparty and is never the identity
* key or a spending key. That restriction is load-bearing rather than stylistic: for a
* counterparty point Q, d*Q IS the ECDH shared secret with Q, so performing this operation
* with a key used for anything else would hand the caller that secret and break encryption
* to that counterparty. A protocol-scoped key that never signs and never encrypts has no
* such property to lose.
*/
// Declared optional (`?`) so that ProtoWallet remains as wide a structural type as it was
// before BRC-229 existed. A required member -- method or property -- narrows what satisfies
// `ProtoWallet`, and every implementor that does not extend the class stops type-checking:
// Wallet, PrivilegedKeyManager and the wallet managers in @bsv/wallet-toolbox all implement
// it structurally. BRC-229 is an opt-in capability, so its presence on the type is opt-in
// too, and callers feature-detect exactly as they do on WalletInterface.
multiplyPoint?: (args: MultiplyPointArgs) => Promise<MultiplyPointResult> = async (
args: MultiplyPointArgs
): Promise<MultiplyPointResult> => {
if (args.protocolID == null || args.keyID == null || args.keyID === '') {
throw new Error('protocolID and keyID are required.')
}
const keyDeriver = keyDeriverOrThrow(this.keyDeriver)
const point = parseValidPoint(args.point)
const derived = derivePrivateKey(
keyDeriver,
args.protocolID,
args.keyID,
args.counterparty ?? 'self'
)

const curve = new Curve()
// With invert, multiply by d^-1 mod n rather than d, so a mask applied under this
// derivation can be stripped by the same wallet.
const scalar = args.invert === true ? derived.invm(curve.n) : new BigNumber(derived.toHex(), 16)

const result = point.mul(scalar)

// Refuse a result at infinity rather than encoding it: it carries no information, and
// every protocol built on this primitive treats it as a failure.
if (result.isInfinity()) {
throw new Error('multiplyPoint: the result is the point at infinity')
}

return { point: result.encode(true, 'hex') as string }
}

async revealCounterpartyKeyLinkage(
args: RevealCounterpartyKeyLinkageArgs
): Promise<RevealCounterpartyKeyLinkageResult> {
Expand Down
47 changes: 47 additions & 0 deletions packages/sdk/src/wallet/Wallet.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,35 @@ export interface WalletEncryptionArgs {
* @param {BooleanDefaultFalse|true} [identityKey] - Use true to retrieve the current user's own identity key, overriding any protocol ID, key ID, or counterparty specified.
* @param {BooleanDefaultFalse} [forSelf] - Whether to return the public key derived from the current user's own identity (as opposed to the counterparty's identity).
*/
/**
* Arguments for BRC-229 wallet-native elliptic curve point multiplication.
*/
export interface MultiplyPointArgs {
/** The point to multiply, as a compressed DER-encoded secp256k1 point. */
point: PubKeyHex
/** BRC-43 security level and protocol ID used to derive the key. */
protocolID: WalletProtocol
/** BRC-43 key ID used to derive the key. */
keyID: KeyIDStringUnder800Bytes
/** Counterparty for derivation. Defaults to 'self'. */
counterparty?: WalletCounterparty
/**
* Multiply by the modular inverse of the derived key instead of the key itself,
* so a mask applied by the wallet can be removed by the wallet.
*/
invert?: BooleanDefaultFalse
privileged?: BooleanDefaultFalse
privilegedReason?: DescriptionString5to50Bytes
seekPermission?: BooleanDefaultTrue
}

/**
* Result of a BRC-229 point multiplication: the resulting point, compressed DER-encoded.
*/
export interface MultiplyPointResult {
point: PubKeyHex
}

export interface GetPublicKeyArgs extends Partial<WalletEncryptionArgs> {
identityKey?: true
forSelf?: BooleanDefaultFalse
Expand Down Expand Up @@ -1025,6 +1054,24 @@ export interface WalletInterface {
originator?: OriginatorDomainNameStringUnder250Bytes
) => Promise<GetPublicKeyResult>

/**
* Multiplies a caller-supplied secp256k1 point by a derived private key, returning the
* resulting point without revealing the key (BRC-229). Set `invert` to multiply by the
* modular inverse instead, undoing a mask previously applied under the same derivation.
*
* Optional. BRC-100 is an unchanging interface, so a method added after the fact cannot be
* mandatory without invalidating every wallet and substrate already shipped. Applications
* MUST feature-detect (`typeof wallet.multiplyPoint === 'function'`) and degrade gracefully.
*
* @param {MultiplyPointArgs} args - The point, the BRC-43 derivation arguments, and options.
* @param {OriginatorDomainNameStringUnder250Bytes} [originator] - FQDN of the originating application.
* @returns {Promise<MultiplyPointResult>} Resolves to the resulting point, or an error response.
*/
multiplyPoint?: (
args: MultiplyPointArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
) => Promise<MultiplyPointResult>

/**
* Reveals the key linkage between ourselves and a counterparty, to a particular verifier, across all interactions with the counterparty.
*
Expand Down
32 changes: 27 additions & 5 deletions packages/sdk/src/wallet/WalletClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ import {
SignActionResult,
VersionString7To30Bytes,
WalletInterface,
AuthenticatedResult
AuthenticatedResult,
MultiplyPointArgs,
MultiplyPointResult
} from './Wallet.interfaces.js'
import WindowCWISubstrate from './substrates/window.CWI.js'
import XDMSubstrate from './substrates/XDM.js'
Expand Down Expand Up @@ -142,10 +144,7 @@ export default class WalletClient implements WalletInterface {
() => new HTTPWalletJSON(this.originator, 'https://localhost:2121'),
MAX_FAST_SUBSTRATE_RESPONSE_WAIT
),
attemptSubstrate(
() => new HTTPWalletJSON(this.originator),
MAX_FAST_SUBSTRATE_RESPONSE_WAIT
),
attemptSubstrate(() => new HTTPWalletJSON(this.originator), MAX_FAST_SUBSTRATE_RESPONSE_WAIT),
attemptSubstrate(
() => new ReactNativeWebView('*', MAX_FAST_SUBSTRATE_RESPONSE_WAIT),
MAX_FAST_SUBSTRATE_RESPONSE_WAIT
Expand Down Expand Up @@ -234,6 +233,29 @@ export default class WalletClient implements WalletInterface {
return await (this.substrate as WalletInterface).getPublicKey(args, this.originator)
}

/**
* BRC-229 point multiplication. Optional across the interface, so this throws a clear error
* when the connected substrate does not implement it rather than failing on `undefined`.
* Applications should feature-detect with {@link WalletClient.supportsMultiplyPoint}.
*/
async multiplyPoint(args: MultiplyPointArgs): Promise<MultiplyPointResult> {
await this.connectToSubstrate()
const substrate = this.substrate as WalletInterface
if (typeof substrate.multiplyPoint !== 'function') {
throw new TypeError('The connected wallet does not implement multiplyPoint (BRC-229)')
}
return await substrate.multiplyPoint(args, this.originator)
}

/**
* Reports whether the connected wallet implements BRC-229 point multiplication, so an
* application can choose a fallback before committing to a protocol that needs it.
*/
async supportsMultiplyPoint(): Promise<boolean> {
await this.connectToSubstrate()
return typeof (this.substrate as WalletInterface).multiplyPoint === 'function'
}

async revealCounterpartyKeyLinkage(args: {
counterparty: PubKeyHex
verifier: PubKeyHex
Expand Down
Loading
Loading