bitcoin-sv / BRCs

Bitcoin Request for Comments
26 stars 13 forks source link

BRC-84 linked keys derivation schema #70

Closed dorzepowski closed 5 months ago

dorzepowski commented 5 months ago

This pull request:

Proposes a new standard by creating a new markdown file in the appropriate directory and requests discussion and assignment of a BRC number

Summary

The Linked Key Derivation Scheme builds on and extends the "type 42" key derivation method. This scheme allows public key derivation using only the public keys of both parties.

ty-everett commented 5 months ago

Within the type-42 architecture, we already have a way of doing this. It's called the anyone key, and the idea is that we use the value 1 for the identity private key of the sender. Then, the value of 1 * G is the "anyone" public key.

This way, we don't have a different algorithm for public-only derivation. If this BRC-84 simply documents an existing process which already has adoption, by all means we should proceed to publication. However, if the goal is simply to enable public-only derivation, it may be better to align on a singular approach to doing this.

What do you think of using 1 as the private key? When Alice communicates with Bob, instead of using her own private key to derive one of Bob's keuys, she replaces it with 1 so that everyone in the world can check that the derivation is correct.

It's also important to communicate about the different security and privacy properties of the two systems (publicly-knowable vs. privately-knowable key derivation). One example implementation is here, where a Gateway.cash payment server derives a publicly-knowable output script under the "anyone key" for a recipient — the security properties are similar to those of BIP32 (not as secure or private as full type-42), even though we maintain the same invoice numbering scheme and derivation algorithm.

For people who like Paymail, you're also welcome to investigate/adapt my Gateway code to derive and validate output scripts for payment destinations on the server.

dorzepowski commented 5 months ago

anyone key

Thank you for your insightful feedback. I agree that having a single derivation scheme is beneficial, but the "anyone key" strategy doesn't fully meet our needs for several reasons:

The main goal of Linked Keys is to maintain a linkage between counterparties' keys when only public keys are used for derivation.

In the anyone key strategy, the new public key could be used by anyone, differentiated only by the invoice number. This makes it possible to enumerate keys if the receiver invoice number follows an easily deducible pattern.

Linked Keys use the public keys of both counterparties, providing two properties to differentiate the derived key: the counterparty's public key and the invoice number. This enhances security and privacy.

While Linked Keys offer lower security than full type 42, they require knowledge of both counterparties' keys and the invoice number, preventing public key enumeration based solely on invoice number patterns.

Apart from that technically, regardless of using "type 42 with anyone key" or Linked Keys, you need to know the approach used to derive the private key. Full type 42 cannot derive private keys for "anyone key".

I hope this clarifies why Linked Keys better fit our requirements. I'm opened for any suggestions how we can align this approach to get "higher" compatibility with type 42 and preserving linking between keys.

dorzepowski commented 5 months ago

I'm opened for any suggestions how we can align this approach to get "higher" compatibility with type 42 and preserving linking between keys.

I have another idea that maybe can meet needs of both of us. But first the context: The main and only difference between type 42 and the solution described in this PR is that "Linked keys" doesn't create shared secret but instead a public key (of the sender) is used as argument for HMAC. Example in code: Type 42

deriveChild (privateKey: PrivateKey, invoiceNumber: string): PublicKey {
    const sharedSecret = this.deriveSharedSecret(privateKey)
    const invoiceNumberBin = toArray(invoiceNumber, 'utf8')
    const hmac = sha256hmac(sharedSecret.encode(true), invoiceNumberBin)
   // the rest doesn't change
}

Linked keys deriveLinkedKey (publicKey: PublicKey, invoiceNumber: string): PublicKey { const invoiceNumberBin = toArray(invoiceNumber, 'utf8') const hmac = sha256hmac(publicKey.encode(true), invoiceNumberBin) // the rest is the same as in type 42 }

Proposition My new idea would reuse whole type 42 algorithm. The idea is to create "linking private key" which would be generated from public key of the sender in the following way:

  1. get compact hex of given public key
  2. threat the hex of public key as a big number
  3. use the big number from serialised public key to create private key
  4. pass this private key to type 42 derivation

Example in code:

  deriveLinkedKey (publicKey: PublicKey, invoiceNumber: string): PublicKey {
    const linkPrivKey = new PrivateKey(publicKey.encode(true), 'hex')
    return this.deriveChild(linkPrivKey, invoiceNumber)
  }
sirdeggen commented 5 months ago

Agree with @dorzepowski 's approach here. We do want a way to further randomize to improve privacy, while acknowledging that truly private linked keys are shared secret based.

I'd say this doesn't stop someone from randomly using this scheme to pretend a payment is coming from someone else, so mitigating that may be something we advocate at some higher level, like signing the request using their key (something anyone can validate against their public key).

I'd update the code slightly to avoid overcomplicating. The public key is two big numbers, so just use one of them:

import { PrivateKey, PublicKey } from '@bsv/sdk'

const beneficiary = PrivateKey.fromRandom()
const originator = PrivateKey.fromRandom()

const beneficiaryPub = beneficiary.toPublicKey()
const originatorPub = originator.toPublicKey()

// lock
const childPub = beneficiaryPub.deriveChild(originatorPub.x, 'invoice 1')

// unlock
const childPriv = beneficiary.deriveChild(new PrivateKey(originatorPub.x).toPublicKey(), 'invoice 1')

console.log({ lock: childPub.toString(), unlock: childPriv.toPublicKey().toString()  })

So the debate is - does it improve anything to use:

new PrivateKey(originatorPub.x)

vs.

new PrivateKey(1)

considering both requests would need to be signed by Alice's key in any case, to prove origin.

sirdeggen commented 5 months ago

The requirement it that we are able to approve an inbound payment on behalf of a user of our hosted wallet such that we validate:

  1. That the recipient can spend the outputs
  2. That the payment is from the originator specified.

I feel like both solutions solve 1. but not 2.

sirdeggen commented 5 months ago

Drawing 3 Possible solution:

import { PrivateKey, PublicKey, Hash, Utils, Curve, BigNumber } from '@bsv/sdk'

const beneficiary = PrivateKey.fromRandom()
const originator = PrivateKey.fromRandom()
const server = PrivateKey.fromRandom()

const beneficiaryPub = beneficiary.toPublicKey()
const serverPub = server.toPublicKey()
const originatorPub = originator.toPublicKey()

const serverSecret = server.deriveSharedSecret(beneficiaryPub)
const d = new PrivateKey(serverSecret.x)
const serverShared = d.toPublicKey()

function deriveLinkedPrivateKey(serverPub, privateKey, counterpartyPublicKey, invoiceNumber) {
  const serverSecret = privateKey.deriveSharedSecret(serverPub)
  const d = new PrivateKey(serverSecret.x)
  const sharedSecret = d.deriveSharedSecret(counterpartyPublicKey)
  const invoiceNumberBin = Utils.toArray(invoiceNumber, 'utf8')
  const hmac = Hash.sha256hmac(sharedSecret.encode(true), invoiceNumberBin)
  const curve = new Curve()
  return new PrivateKey(privateKey.add(new BigNumber(hmac)).mod(curve.n).toArray())
}

function deriveLinkedPublicKey(serverShared, privateKey, counterpartyPublicKey, invoiceNumber) {
  const sharedSecret = privateKey.deriveSharedSecret(serverShared)
  const invoiceNumberBin = Utils.toArray(invoiceNumber, 'utf8')
  const hmac = Hash.sha256hmac(sharedSecret.encode(true), invoiceNumberBin)
  const curve = new Curve()
  const point = curve.g.mul(new BigNumber(hmac))
  const finalPoint = counterpartyPublicKey.add(point)
  return new PublicKey(finalPoint.x, finalPoint.y)
}

const payToThis = deriveLinkedPublicKey(serverShared, originator, beneficiaryPub, 'invoice 1')
const unlockWithThis = deriveLinkedPrivateKey(serverPub, beneficiary, originatorPub, 'invoice 1')

console.log({ payToThis: payToThis.toString(), unlockWithThis: unlockWithThis.toPublicKey().toString() })
sirdeggen commented 5 months ago

What the above allows is:

  1. Alice can generate a linked key with the server while Bob is offline.
  2. Server can validate inbound transactions are linked to Bob on his behalf.
  3. Alice and Bob both have information which links the transaction to one another.
  4. Random 3rd parties cannot link transactions because they don't know the share secret between Bob and his Server.
sirdeggen commented 5 months ago

FAO @dorzepowski

dorzepowski commented 5 months ago

The requirement it that we are able to approve an inbound payment on behalf of a user of our hosted wallet such that we validate:

  1. That the recipient can spend the outputs
  2. That the payment is from the originator specified.

I feel like both solutions solve 1. but not 2.

After some thinking I'm not sure if I get the context of the problem exactly.

So getting back to the question, what actually we want to achieve, If we want to solve point 2, the the answer in Paymail is the capability 6745385c3fc0.

sirdeggen commented 5 months ago

If we want to solve point 2, the the answer in Paymail is the capability 6745385c3fc0

No for two reasons

  1. Basic Address resolution is deprecated
  2. Signatures in paymail requests are not immutable.

The benefit of 42 style key derivations is that you have an immutable link which is entirely obscured yet auditable. The output is indistinguishable from any other, yet either party can link their metadata to the transaction post hoc on demand.

sirdeggen commented 5 months ago
  • We have Alice, Bob, and Charlie (who is some bad guy)

Yes

  • If Charlie will derive Linked key for Bob and send the money to Bob, still Bob is in charge of the money.

Yes

  • If we're assuming that Bob is a problem also (because [Charlie is] cooperating with Bob), then having keys in the wallet doesn't solve anything because Bob still is able to derive public key from Alice and Server public key.
  • what's more, if the Bob is a problem, then even full type 42 has the same issue, because still Bob can derive public key without Alice engagement

Bob can certainly derive a public key for Alice, without her interaction. What he can't do is force her to accept it. Her server can reject it because it can prove a link between Bob and Alice -- if Alice has blocked him or otherwise not approved him as a valid originator, then the server can reject the payment on that basis.

  • last but not least: IMHO there is no solution to force the originator to be Alice at the BSV level, because there is nothing that could prevent Charlie and Bob (working together) from taking already existing address that Alice used from whatsonchain, and make a transaction to Bob on the same address.

This thought is in the mindset of listening to all transactions - old think - we are in the SPV era - payments to the wallet are only valid if sent to and accepted by that wallet. Transactions to old keys are simply ignored.

sirdeggen commented 5 months ago
H + B

This point addition is what makes sure the funds are only spendable by Bob.

The concern otherwise is only for "who can link this key to an event or person".

Shared secrets allow two parties to link the keys. The scheme I described above allows three parties to link the keys to Alice Bob and an invoice number.

  1. Alice because she has a her privkey and D the public key associated with F the shared secret between Bob and the Server.
  2. Bob because he has d the privkey associated with F the shared secret between him and the Server.
  3. The Server because it has d the privkey associated with F the shared secret between it and Bob.

No one outside of this three has the information required to link the key to Alice or Bob. Even if they have the invoice schema.

dorzepowski commented 5 months ago

I would like to rephrase what you said to make sure I understood it correctly. Our goal is to have the following properties of the derived public key:

  1. It links the sender and receiver
  2. only counterparties can prove linkage between counterparties
  3. Receiving a payment when receiver is offline should be possible

@sirdeggen can you confirm that this is all we need, or I'm still missing something

ty-everett commented 5 months ago

Have we considered the use of BRC-69 (method 2) key linkage revelation? You can maintain the full privacy benefits of type-42 derivation, preserve a cryptographic link between the keys, and the server is able to authenticate the linked child key.

https://bsv.brc.dev/key-derivation/0069

  1. Alice learns Bob's identity key for a payment
  2. Alice uses type-42 derivation as normal, with her private key and Bob's public key, and some invoice number
  3. Alice creates a blockchain transaction involving a payment to Bob's derived child key
  4. Alice uses some authentication protocol (like BRC-31 or a challenge-response) to prove to the server that she controls her identity key
  5. Alice sends the transaction, the invoice number, and the BRC-69 method 2 key linkage material to the server, signing this data for the server's and Bob's records
  6. The server verifies that adding Bob's identity key with the provided linkage material results in the same derived child key used in the transaction

At this stage, several things are true:

However, there are some weaknesses:

There are I think two ways to address this issue:

  1. Resort to the use of method 1 revelation, exposing the root shared secret between the parties to the server so that it can verify the computation of linkage offsets, or
  2. Make use of some sort of zero-knowledge proof, as alluded to above.

I think this is one of the rare cases where a zero-knowledge proof may actually be useful and necessary, if one can be made.

https://chatgpt.com/share/c2242c59-88a2-4256-94b0-37a50797e482

The obvious advantage to this approach is privacy, and the obvious disadvantage is added complexity.

ty-everett commented 5 months ago

I think there's a lot of valuable stuff here that we need to digest more fully. I propose that we:

  1. Merge this BRC as is, so that people can use it if they want — with the understanding that there's no way to authenticate the sender
  2. @sirdeggen your idea with the three-party scheme seems interesting, and it may be valuable if you memorialize it into a new BRC for future reference. It's a bit hard for me to get my head around and could do with some further explanation.
  3. Finally, I will write three new BRCs as I have the time. One for a better way of bidirectionally authenticating in a reduced privacy approach by embedding a signature and key into the invoice number itself, one making use of BRC-69 linkage revelation for enhanced privacy and limited verification as described above, and one exploring zero knowledge proofs.
dorzepowski commented 5 months ago

I think there's a lot of valuable stuff here that we need to digest more fully. I propose that we:

  1. Merge this BRC as is, so that people can use it if they want — with the understanding that there's no way to authenticate the sender
  2. @sirdeggen your idea with the three-party scheme seems interesting, and it may be valuable if you memorialize it into a new BRC for future reference. It's a bit hard for me to get my head around and could do with some further explanation.
  3. Finally, I will write three new BRCs as I have the time. One for a better way of bidirectionally authenticating in a reduced privacy approach by embedding a signature and key into the invoice number itself, one making use of BRC-69 linkage revelation for enhanced privacy and limited verification as described above, and one exploring zero knowledge proofs.

I agree with the proposition to merge this PR, we will get back to that topic of 3 party confirmation within a month or so, maybe then we will propose version 2 of this BRC

sirdeggen commented 4 months ago

The issue remaining is that the actual document isn't showing up in the brc gitbook. Weird.

sirdeggen commented 4 months ago

Glad to have captured this conversation here though. I'll write up those BRCs discussed when it comes to me.

sirdeggen commented 1 month ago

Drawing 3

Capturing good feedback sent in a dm from Jonathan Vaage:

I feel it’s worth pointing out that Bob would probably want to sign a ‘delegation’ addendum to his root identity key registration that claims D as a sub-identity associated with the context under purview of the hosted wallet W. This would deter the wallet service from distributing another public key not derived from B as Alice would have no other way of verifying that key. Granted, even a malicious wallet service couldn’t steal funds from Bob by providing an incorrect key in place of D and they are already being trusted to faithfully deliver transactions to Bob while he’s offline, so I wouldn’t say it’s absolutely critical for security, but might still be worthwhile as a signal to Alice that the wallet service has in fact been authorized to facilitate and have visibility of payments to Bob.