@@ -41,6 +41,15 @@ case class PayerProof(records: TlvStream[PayerProofTlv]) {
4141 PayerProof .validate(records) match {
4242 case Left (_) => false
4343 case Right (_) =>
44+ // The proof signature must be valid: it signs all TLVs (except itself) using the invreq_payer_id,
45+ // with an empty invreq_metadata field (to prevent brute-forcing omitted fields).
46+ val payerSig = records.get[ProofSignature ].get.signature
47+ val payerId = records.get[InvoiceRequestPayerId ].get.publicKey
48+ val proofRootHash = rootHash(TlvStream (records.records.filterNot(_.isInstanceOf [ProofSignature ]), records.unknown), OfferCodecs .payerProofCodec)
49+ if (! verifySchnorr(PayerProof .signatureTag, proofRootHash, payerSig, payerId)) {
50+ return false
51+ }
52+ // The proof must contain enough data to reconstruct the invoice merkle root.
4453 val leafNonces = records.get[LeafHashes ].map(_.hashes).getOrElse(Nil )
4554 val markers = records.get[OmittedTlvs ].map(_.missing).getOrElse(Nil )
4655 val missingHashes = records.get[MissingHashes ].map(_.missing).getOrElse(Nil )
@@ -66,7 +75,10 @@ case class PayerProof(records: TlvStream[PayerProofTlv]) {
6675 val omittedLeavesOk = allLeavesExceptMetadata.zipWithIndex.forall {
6776 case ((_, Some (_)), _) => true
6877 case ((marker, None ), idx) if idx == 0 => marker == UInt64 (1 )
69- case ((marker, None ), idx) => marker == (allLeavesExceptMetadata(idx - 1 )._1 + 1 )
78+ case ((marker, None ), idx) =>
79+ val previous = allLeavesExceptMetadata(idx - 1 )._1
80+ val expected = if (previous == UInt64 (239 )) UInt64 (1_000_000_000 ) else previous + 1
81+ marker == expected
7082 }
7183 if (! omittedLeavesOk) {
7284 return false
@@ -75,22 +87,11 @@ case class PayerProof(records: TlvStream[PayerProofTlv]) {
7587 val allLeaves = (Option .empty[LeafWithNonce ] +: allLeavesExceptMetadata.map(_._2)).toIndexedSeq
7688 PayerProofTree .merkleRoot(allLeaves, missingHashes) match {
7789 case Left (_) => false
78- case Right (rootHash) =>
90+ case Right (invoiceRootHash) =>
91+ // The invoice signature must be valid against invoice_node_id over the reconstructed merkle root.
7992 val invoiceSig = records.get[Signature ].get.signature
8093 val invoiceNodeId = records.get[InvoiceNodeId ].get.nodeId
81- if (! verifySchnorr(Bolt12Invoice .signatureTag, rootHash, invoiceSig, invoiceNodeId)) {
82- // The invoice signature must be valid against invoice_node_id over the reconstructed merkle root.
83- false
84- } else {
85- // The payer signature must be valid against invreq_payer_id over SHA256(note || merkle_root).
86- val payerSig = records.get[PayerSignature ].get.signature
87- val payerId = records.get[InvoiceRequestPayerId ].get.publicKey
88- val msg = records.get[PayerSignature ].flatMap(_.note_opt) match {
89- case Some (note) => Crypto .sha256(ByteVector (note.getBytes) ++ rootHash)
90- case None => Crypto .sha256(rootHash)
91- }
92- verifySchnorr(PayerProof .signatureTag, msg, payerSig, payerId)
93- }
94+ verifySchnorr(Bolt12Invoice .signatureTag, invoiceRootHash, invoiceSig, invoiceNodeId)
9495 }
9596 }
9697 }
@@ -105,7 +106,7 @@ case class PayerProof(records: TlvStream[PayerProofTlv]) {
105106object PayerProof {
106107
107108 val hrp = " lnp"
108- val signatureTag : ByteVector = ByteVector ((" lightning" + " payer_proof" + " payer_signature " ).getBytes)
109+ val signatureTag : ByteVector = ByteVector ((" lightning" + " payer_proof" + " proof_signature " ).getBytes)
109110
110111 /** Specifies which optional fields from the invoice should be included in the payer proof. */
111112 case class IncludedFields (offerChains : Boolean = false ,
@@ -134,6 +135,8 @@ object PayerProof {
134135 unknown : Set [UInt64 ] = Set .empty)
135136
136137 def create (invoice : Bolt12Invoice , preimage : ByteVector32 , payerKey : PrivateKey , fields : IncludedFields , note_opt : Option [String ]): PayerProof = {
138+ // Valid Bolt12 invoices always contain the invreq_metadata field: it is used as a hashing nonce to prevent brute-forcing private fields.
139+ val invreqMetadata = invoice.records.get[InvoiceRequestMetadata ].get
137140 // We select invoice fields that we want to include in our payer proof.
138141 val knownLeaves : Set [(InvoiceTlv , Boolean )] = invoice.records.records
139142 .filterNot(_.isInstanceOf [Signature ])
@@ -179,8 +182,6 @@ object PayerProof {
179182 .toSeq
180183 .sortBy(_._1.tag)
181184 .map { case (tlv, included) => PayerProofTree .Leaf (tlv, included) }
182- // The invreq_metadata field must be provided (required in Bolt12Invoice).
183- val invreqMetadata = invoice.records.get[InvoiceRequestMetadata ].get
184185 // We include the leaf nonce for each invoice TLV we include in the payer proof.
185186 val leafNonces = leaves.collect {
186187 case leaf if leaf.included => PayerProofTree .leafNonce(invreqMetadata, leaf.tlv)
@@ -197,26 +198,29 @@ object PayerProof {
197198 // This TLV is not included in the payer proof: we add an entry with a marker.
198199 val markerNumber = previousTlv_opt match {
199200 case Some (tag) => tag + 1
200- case None => omitted.lastOption.map(tag => tag + 1 ).getOrElse(UInt64 (1 ))
201+ case None => omitted.lastOption match {
202+ case Some (tag) if tag == UInt64 (239 ) => UInt64 (1_000_000_000 )
203+ case Some (tag) => tag + 1
204+ case None => UInt64 (1 )
205+ }
201206 }
202207 (omitted :+ markerNumber, None )
203208 }._1.toList
204209 val proofTree = PayerProofTree (leaves)
205210 val missingHashes = PayerProofTree .computeMissingHashes(proofTree, invreqMetadata)
206- val payerSig = note_opt match {
207- case Some (note) => signSchnorr(signatureTag, Crypto .sha256(ByteVector (note.getBytes) ++ proofTree.hash(invreqMetadata)), payerKey)
208- case None => signSchnorr(signatureTag, Crypto .sha256(proofTree.hash(invreqMetadata)), payerKey)
209- }
210211 val includedInvoiceTlvs = knownLeaves.collect { case (tlv, true ) => tlv }.toSet[PayerProofTlv ]
211212 val payerProofTlvs = Set (
212213 Some (InvoicePreimage (preimage)),
213214 Some (LeafHashes (leafNonces)),
214215 if (omittedTlvs.nonEmpty) Some (OmittedTlvs (omittedTlvs)) else None ,
215216 if (missingHashes.nonEmpty) Some (MissingHashes (missingHashes)) else None ,
216- Some ( PayerSignature (payerSig, note_opt))
217+ note_opt.map(note => ProofNote (note)),
217218 ).flatten[PayerProofTlv ]
218219 val includedUnknownTlvs = unknownLeaves.collect { case (tlv, true ) => tlv }
219- PayerProof (TlvStream (includedInvoiceTlvs ++ invoice.records.get[Signature ].toSet ++ payerProofTlvs, includedUnknownTlvs))
220+ val tlvsWithoutProofSig = TlvStream (includedInvoiceTlvs ++ invoice.records.get[Signature ].toSet ++ payerProofTlvs, includedUnknownTlvs)
221+ // We use a static 0x0000 hashing nonce instead of the invreq_metadata, which we don't want to disclose.
222+ val proofSig = signSchnorr(signatureTag, rootHash(tlvsWithoutProofSig, OfferCodecs .payerProofCodec), payerKey)
223+ PayerProof (tlvsWithoutProofSig.copy(records = tlvsWithoutProofSig.records + ProofSignature (proofSig)))
220224 }
221225
222226 /** When validating a proof, the leaf nonce is directly provided to avoid brute-forcing omitted fields. */
@@ -354,15 +358,14 @@ object PayerProof {
354358 if (records.get[InvoicePaymentHash ].isEmpty) return Left (MissingRequiredTlv (UInt64 (168 )))
355359 if (records.get[InvoiceNodeId ].isEmpty) return Left (MissingRequiredTlv (UInt64 (176 )))
356360 if (records.get[Signature ].isEmpty) return Left (MissingRequiredTlv (UInt64 (240 )))
357- if (records.get[InvoicePreimage ].isEmpty) return Left (MissingRequiredTlv (UInt64 (242 )))
358- if (records.get[PayerSignature ].isEmpty) return Left (MissingRequiredTlv (UInt64 (250 )))
361+ if (records.get[ProofSignature ].isEmpty) return Left (MissingRequiredTlv (UInt64 (241 )))
362+ if (records.get[InvoicePreimage ].isEmpty) return Left (MissingRequiredTlv (UInt64 (1001 )))
359363 // The preimage must match the invoice's payment_hash.
360- if (Crypto .sha256(records.get[InvoicePreimage ].get.preimage) != records.get[InvoicePaymentHash ].get.hash) return Left (InvalidTlvValue (UInt64 (242 )))
364+ if (Crypto .sha256(records.get[InvoicePreimage ].get.preimage) != records.get[InvoicePaymentHash ].get.hash) return Left (InvalidTlvValue (UInt64 (1001 )))
361365 val omittedTlvs = records.get[OmittedTlvs ].map(_.missing).getOrElse(Nil )
362- if (omittedTlvs.length != omittedTlvs.distinct.length) return Left (InvalidTlvValue (UInt64 (244 )))
363- if (omittedTlvs.sorted != omittedTlvs) return Left (InvalidTlvValue (UInt64 (244 )))
364- if (omittedTlvs.contains(UInt64 (0 ))) return Left (InvalidTlvValue (UInt64 (244 )))
365- if (omittedTlvs.exists(tag => tag >= UInt64 (240 ) && tag <= UInt64 (1000 ))) return Left (InvalidTlvValue (UInt64 (244 )))
366+ if (omittedTlvs.length != omittedTlvs.distinct.length) return Left (InvalidTlvValue (UInt64 (1002 )))
367+ if (omittedTlvs.sorted != omittedTlvs) return Left (InvalidTlvValue (UInt64 (1002 )))
368+ if (! omittedTlvs.forall(tag => (tag >= UInt64 (1 ) && tag <= UInt64 (239 )) || (tag >= UInt64 (1_000_000_000L ) && tag <= UInt64 (3_999_999_999L )))) return Left (InvalidTlvValue (UInt64 (1002 )))
366369 Right (PayerProof (records))
367370 }
368371
0 commit comments