Closed dorzepowski closed 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.
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.
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:
Example in code:
deriveLinkedKey (publicKey: PublicKey, invoiceNumber: string): PublicKey {
const linkPrivKey = new PrivateKey(publicKey.encode(true), 'hex')
return this.deriveChild(linkPrivKey, invoiceNumber)
}
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.
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:
I feel like both solutions solve 1. but not 2.
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() })
What the above allows is:
FAO @dorzepowski
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:
- That the recipient can spend the outputs
- 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.
If we want to solve point 2, the the answer in Paymail is the capability 6745385c3fc0
No for two reasons
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.
- 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.
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.
a
her privkey and D
the public key associated with F
the shared secret between Bob and the Server. d
the privkey associated with F
the shared secret between him and the Server.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.
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:
@sirdeggen can you confirm that this is all we need, or I'm still missing something
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
At this stage, several things are true:
However, there are some weaknesses:
There are I think two ways to address this issue:
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.
I think there's a lot of valuable stuff here that we need to digest more fully. I propose that we:
I think there's a lot of valuable stuff here that we need to digest more fully. I propose that we:
- 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
- @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.
- 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
The issue remaining is that the actual document isn't showing up in the brc gitbook. Weird.
Glad to have captured this conversation here though. I'll write up those BRCs discussed when it comes to me.
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.
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.