smite: extend accept_channel oracle with key reuse checks - #259
NishantBansal2003 wants to merge 4 commits into
Conversation
cc96bd6 to
2a6f201
Compare
Record each pubkey sent in open_channel, accept_channel and channel_ready with its origin (side, channel, field). We may send the same pubkey more than once, so ours can have several origins. The target must never send a pubkey already sent by either side, so each of its pubkeys has exactly one origin. This prepares for an oracle that detects pubkey reuse by the target. Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Report accept_channel messages that use a pubkey already revealed by either side, or share one across their own fields. Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
2a6f201 to
57b44fa
Compare
| // Check that each pubkey is unrevealed. | ||
| for (field, pubkey) in &pubkeys { | ||
| if let Some(origins) = revealed_pubkeys.get(pubkey) { | ||
| let origins: Vec<String> = origins.iter().map(ToString::to_string).collect(); | ||
| return Err(format!( | ||
| "{field} {pubkey} was already sent as {}", | ||
| origins.join(", "), | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| // Check that each pubkey appears in only one field. | ||
| let mut fields = HashMap::new(); | ||
| for (field, pubkey) in &pubkeys { | ||
| if let Some(other_field) = fields.insert(pubkey, field) { | ||
| return Err(format!("{other_field} and {field} share pubkey {pubkey}")); | ||
| } | ||
| } |
There was a problem hiding this comment.
Nit: these loops can be combined.
| // Check that each pubkey is unrevealed. | |
| for (field, pubkey) in &pubkeys { | |
| if let Some(origins) = revealed_pubkeys.get(pubkey) { | |
| let origins: Vec<String> = origins.iter().map(ToString::to_string).collect(); | |
| return Err(format!( | |
| "{field} {pubkey} was already sent as {}", | |
| origins.join(", "), | |
| )); | |
| } | |
| } | |
| // Check that each pubkey appears in only one field. | |
| let mut fields = HashMap::new(); | |
| for (field, pubkey) in &pubkeys { | |
| if let Some(other_field) = fields.insert(pubkey, field) { | |
| return Err(format!("{other_field} and {field} share pubkey {pubkey}")); | |
| } | |
| } | |
| // Check that each pubkey is unrevealed and appears in only one field. | |
| let mut seen = HashMap::new(); | |
| for (field, pubkey) in accept_channel.pubkeys() { | |
| if let Some(origins) = revealed_pubkeys.get(pubkey) { | |
| let origins: Vec<String> = origins.iter().map(ToString::to_string).collect(); | |
| return Err(format!( | |
| "{field} {pubkey} was already sent as {}", | |
| origins.join(", "), | |
| )); | |
| } | |
| if let Some(other_field) = seen.insert(pubkey, field) { | |
| return Err(format!("{other_field} and {field} share pubkey {pubkey}")); | |
| } | |
| } |
There was a problem hiding this comment.
Yes, but the reason to keep them separate is that if a pubkey is reused from an earlier negotiation, where its secret has already been revealed (eg in commitment dance), that is a much more serious bug than a key being reused within the same negotiation
| /// Every pubkey sent on the wire by either side, mapped to each of its | ||
| /// origins. We may send the same pubkey multiple times, so ours can have | ||
| /// multiple origins. The target must never send a pubkey already sent by | ||
| /// either side, so each of theirs has exactly one origin. | ||
| revealed_pubkeys: HashMap<PublicKey, Vec<KeyOrigin>>, |
There was a problem hiding this comment.
Do we really need to track multiple origins? I'd be happy with just the first one getting printed in the error message since it simplifies the code.
There was a problem hiding this comment.
My thinking is that tracking only the first origin is not really useful, the reasons are:
- This key-reuse check is likely to be non-deterministic (or maybe it would be deterministic in Bedrock? Not sure)
- If the target reuses a pubkey from its own previously sent pubkeys, we can be fairly sure that there is an issue somewhere in its key generation in that path. But if the target generates a key that we previously sent to it, I don't think we would be able to pinpoint the issue if we only track the first origin
There was a problem hiding this comment.
If it's nondeterministic, that probably means the target has weak entropy, in which case I doubt having multiple origins helps.
If the target reuses one of our keys, can't we easily check the program to any additional places we send them our key, if needed?
In every case I can think of, one origin is enough to diagnose the bug.
4b5f3b9 to
6ae5679
Compare
| /// Every pubkey sent on the wire by either side, mapped to each of its | ||
| /// origins. We may send the same pubkey multiple times, so ours can have | ||
| /// multiple origins. The target must never send a pubkey already sent by | ||
| /// either side, so each of theirs has exactly one origin. | ||
| revealed_pubkeys: HashMap<PublicKey, Vec<KeyOrigin>>, |
There was a problem hiding this comment.
If it's nondeterministic, that probably means the target has weak entropy, in which case I doubt having multiple origins helps.
If the target reuses one of our keys, can't we easily check the program to any additional places we send them our key, if needed?
In every case I can think of, one origin is enough to diagnose the bug.
| // The target must never send a pubkey already sent by either side, so the | ||
| // `second_per_commitment_point` is recorded with this as its only origin. | ||
| // | ||
| // TODO: Once we have the channel_ready oracle, update this to return a | ||
| // violation from the target instead of panicking. | ||
| assert!( | ||
| !revealed_pubkeys.contains_key(&cr.second_per_commitment_point), | ||
| "target sent an already sent pubkey", | ||
| ); |
There was a problem hiding this comment.
The spec actually allows second_per_commitment_point repeats: https://github.com/lightning/bolts/blob/1aadb719b4007c4cea0ba6e36b08c4fb53788dee/02-peer-protocol.md?plain=1#L1091
I think we'll be able to handle that in the channel_ready oracle, but in the meantime we shouldn't panic here. For this PR we should also swap the should_panic executor test with one that ensures channel_ready can be resent with a new alias, and we can add current test back in with the channel_ready oracle.
Also once we implement reconnect I think there will be other spec-compliant ways to get repeated points.
There was a problem hiding this comment.
I think for reuse, we could check the whole origin along with the key. If both the key and origin match the new one, it means the pubkey was simply sent again for the same message and field. Otherwise, it is a violation. Once we have the commitment scenario, we could also add the commitment number to the origin and track and compare the origin with respect to the commitment number.
| let (mut fx, _) = recv_channel_ready_fixture(); | ||
| let mut sk_bytes = [0u8; 32]; | ||
| sk_bytes[31] = 1; | ||
| let oc = SampleOpenChannel::new(PointSource::Secret(sk_bytes)); |
There was a problem hiding this comment.
| let (mut fx, _) = recv_channel_ready_fixture(); | |
| let mut sk_bytes = [0u8; 32]; | |
| sk_bytes[31] = 1; | |
| let oc = SampleOpenChannel::new(PointSource::Secret(sk_bytes)); | |
| let (mut fx, target_pcp) = recv_channel_ready_fixture(); | |
| let mut sk_bytes = [0u8; 32]; | |
| sk_bytes[31] = 1; | |
| let oc = SampleOpenChannel::new(PointSource::Secret(sk_bytes)); | |
| assert_eq!(target_pcp, oc.funding_pubkey); |
|
|
||
| fx.run(&b.build()); | ||
| } | ||
|
|
There was a problem hiding this comment.
The fixup removed a bunch of test coverage that we should still be able to recover using public-API tests.
- target's
channel_readyreusesaccept_channelPCP - target's
accept_channelreusesaccept_channelpubkey - target's
channel_readyreuses ourchannel_readyPCP - target's
accept_channelreuseschannel_readyPCP
There was a problem hiding this comment.
Added for accept_channel pubkey reuse checks, since we removed the assert for channel_ready pubkey reuse. I will add those checks to the channel_ready oracle
| //! Remembers the `open_channel`/`accept_channel` parameters of each channel | ||
| //! being established, so later steps can build commitments from them. |
There was a problem hiding this comment.
Doc comments needs update.
| to_self_delay: 144, | ||
| max_accepted_htlcs: 483, | ||
| funding_pubkey: sample_pubkey(1), | ||
| funding_pubkey: sample_pubkey(7), |
There was a problem hiding this comment.
Let's add a comment explaining the reason the keys go 7-2-3-4-... instead of 1-2-3-4-...
ref: #209 (comment)
Some more oracles added in this PR:
open_channelchain_hash != regtestchannel_reserve_satoshis >= funding_satoshisdust_limit_satoshis > 10_000satto_self_delay > 2016feerate_per_kw == 0for non 0FC channelsaccept_channeldust_limit_satoshis > 10_000sathtlc_minimum_msat > max_htlc_value_in_flight_msathtlc_minimum_msat > open_channel.funding_satoshisto_self_delay == 0max_accepted_htlcs == 0open_channelandaccept_channelper_commitment_pointshould not be reusedNotes
to_self_delayto 6 inaccept_channelon regtest and 144 on mainnet, so I think we should only check thatto_self_delay != 0feerate_per_kwfloor to 1 on regtest and 253 on mainnet when accepting theopen_channelmessage, so I think we should only check thatfeerate_per_kw != 0