@@ -557,7 +557,7 @@ removeNoiseEndpoint(transport)
557557 val proposalPath = " $proposalsPath$proposalId "
558558 val envelopeBytes = pubkyStorage.retrieve(proposalPath, adapter, peerPubkey)
559559 val envelopeJson = envelopeBytes?.let { String (it) }
560- decryptAndParseSubscriptionProposal(proposalId, envelopeJson, myPubkey)
560+ decryptAndParseSubscriptionProposal(proposalId, envelopeJson, myPubkey, peerPubkey )
561561 } catch (e: Exception ) {
562562 Logger .error(" Failed to parse proposal $proposalId " , e, context = TAG )
563563 null
@@ -592,7 +592,8 @@ removeNoiseEndpoint(transport)
592592 val proposalPath = " $proposalsPath$proposalId "
593593 val envelopeBytes = pubkyStorage.retrieve(proposalPath, adapter, ownerPubkey)
594594 val envelopeJson = envelopeBytes?.let { String (it) }
595- decryptAndParseSubscriptionProposal(proposalId, envelopeJson, ownerPubkey)
595+ // NOTE: This deprecated method cannot verify provider binding since we don't know who we're polling
596+ decryptAndParseSubscriptionProposal(proposalId, envelopeJson, ownerPubkey, expectedProviderPubkey = null )
596597 } catch (e: Exception ) {
597598 Logger .error(" Failed to parse proposal $proposalId " , e, context = TAG )
598599 null
@@ -688,7 +689,7 @@ removeNoiseEndpoint(transport)
688689 return try {
689690 val envelopeBytes = pubkyStorage.retrieve(proposalPath, adapter, providerPubkey)
690691 val envelopeJson = envelopeBytes?.let { String (it) }
691- decryptAndParseSubscriptionProposal(proposalId, envelopeJson, subscriberPubkey)
692+ decryptAndParseSubscriptionProposal(proposalId, envelopeJson, subscriberPubkey, providerPubkey )
692693 } catch (e: Exception ) {
693694 Logger .error(" Failed to fetch subscription proposal $proposalId " , e, context = TAG )
694695 null
@@ -786,12 +787,14 @@ removeNoiseEndpoint(transport)
786787 * @param proposalId The proposal ID
787788 * @param envelopeJson The JSON string of the sealed blob (or null)
788789 * @param subscriberPubkey Our pubkey (used for canonical AAD computation)
789- * @return The parsed proposal or null if decryption/parsing fails
790+ * @param expectedProviderPubkey If provided, verifies that provider_pubkey in the proposal matches this value
791+ * @return The parsed proposal or null if decryption/parsing/validation fails
790792 */
791793 private fun decryptAndParseSubscriptionProposal (
792794 proposalId : String ,
793795 envelopeJson : String? ,
794796 subscriberPubkey : String ,
797+ expectedProviderPubkey : String? = null,
795798 ): to.bitkit.paykit.workers.DiscoveredSubscriptionProposal ? {
796799 if (envelopeJson.isNullOrBlank()) return null
797800
@@ -816,9 +819,24 @@ removeNoiseEndpoint(transport)
816819 val plaintextJson = String (plaintextBytes)
817820 val obj = org.json.JSONObject (plaintextJson)
818821
822+ val providerPubkey = obj.optString(" provider_pubkey" , " " )
823+
824+ // SECURITY: Verify provider identity binding
825+ if (expectedProviderPubkey != null && providerPubkey.isNotEmpty()) {
826+ val normalizedExpected = PaykitV0Protocol .normalizePubkeyZ32(expectedProviderPubkey)
827+ val normalizedActual = PaykitV0Protocol .normalizePubkeyZ32(providerPubkey)
828+ if (normalizedExpected != normalizedActual) {
829+ Logger .error(
830+ " Provider identity mismatch for proposal $proposalId : expected $normalizedExpected , got $normalizedActual " ,
831+ context = TAG ,
832+ )
833+ return null
834+ }
835+ }
836+
819837 to.bitkit.paykit.workers.DiscoveredSubscriptionProposal (
820838 subscriptionId = proposalId,
821- providerPubkey = obj.optString( " provider_pubkey " , " " ) ,
839+ providerPubkey = providerPubkey ,
822840 amountSats = obj.optLong(" amount_sats" , 0 ),
823841 description = if (obj.has(" description" )) obj.getString(" description" ) else null ,
824842 frequency = obj.optString(" frequency" , " monthly" ),
0 commit comments