Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Truncate secret hash #722

Merged
merged 2 commits into from
Aug 25, 2024

Conversation

tcharding
Copy link
Member

@tcharding tcharding commented Aug 22, 2024

The core::hash::Hasher and bitcoin_hashes hash types implement formatting traits slightly differently

  • We default to displaying in hex but core defaults to using base 10
  • We truncate with precision not width parameter but core truncates with both

Anywho, this PR fixes the secret display truncation.

@tcharding
Copy link
Member Author

@tcharding
Copy link
Member Author

This is non-urgent so I'll just wait till #699 goes in to debug CI failure.

@tcharding tcharding marked this pull request as draft August 22, 2024 02:21
@Kixunil
Copy link
Collaborator

Kixunil commented Aug 22, 2024

hash is a 32 byte integer (I think) and for integral types precision is ignorded

It is but I think many people perceive it mainly as string. Though if we were to truncate it it's unclear if we should truncate the beginning or the end.

src/secret.rs Outdated Show resolved Hide resolved
src/secret.rs Outdated Show resolved Hide resolved
@apoelstra
Copy link
Member

A hash is not an integer. In no sense is a hash an integer. You can interpret a hash as an integer but then you need to make a bunch of decisions about endianness.

A hash is an opaque bytestring. If you truncate it then you should take the prefix not the suffix. This is what git does, this is what Bitcoin does in multiple places (e.g. bip32 fingerprints, the sha256 "checksum" on base58 addresses, etc), and this is what's done to construct "truncated hashes".

@Kixunil
Copy link
Collaborator

Kixunil commented Aug 22, 2024

@apoelstra this is also what GPG doesn't do - the short key identifiers are suffixes. ;) Yes, I hate it.

Anyway, I do think we should allow it.

@apoelstra
Copy link
Member

Hah, TIL another crazy thing about GPG. (I guess I did know this actually, and have been annoyed about it because how hard it is visually to compare the short ID to the long fingerprint.)

@tcharding
Copy link
Member Author

A hash is not an integer.

TIL, thanks. Raised issue: rust-bitcoin/hex-conservative#101

@tcharding tcharding marked this pull request as ready for review August 22, 2024 23:12
In order correctly truncate the secret data we need to use recent
version of `bitcoin_hashes`.

Remove the range dependency and update the lock files.
Currently we are attempting to truncate the hash created using
`bitcoin_hashes` by using the "width" formatting parameter instead of
the "precision" parameter. `hex-conservative` truncates with the
"precision" parameter as is expected since a hash is not an integral
type.

Use the formatting string `"{:.16}"` which is the "precision"
formatting parameter.
@tcharding
Copy link
Member Author

Buuuuut, we'd have to remove the range dependency on bitcoin_hashes.

@apoelstra
Copy link
Member

Let's just remove the range dependency. It doesn't work properly anyway. See #673. I thought we'd removed it already because of that issue.

@apoelstra
Copy link
Member

Not sure what's up with CI. I think maybe nightly has broken something xargo-related?

@tcharding
Copy link
Member Author

Seems so, this works

RUSTUP_TOOLCHAIN=nightly-2024-08-04 xargo run --release --target=x86_64-unknown-linux-gnu

@tcharding
Copy link
Member Author

That is the nightly version currently used in #699

@Kixunil
Copy link
Collaborator

Kixunil commented Aug 23, 2024

WHAT?! What's the problem with ranged dependency?

Actually we made other changes that make #673 non-issue, but there's still #723. I'd like to use ranged dependencies for all private dependencies to minimize dependency duplication.

@@ -37,7 +37,7 @@ global-context-less-secure = ["global-context"]
secp256k1-sys = { version = "0.10.0", default-features = false, path = "./secp256k1-sys" }
serde = { version = "1.0.103", default-features = false, optional = true }

hashes = { package = "bitcoin_hashes", version = ">= 0.12, <= 0.14", default-features = false, optional = true }
hashes = { package = "bitcoin_hashes", version = "0.14", default-features = false, optional = true }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe moving the lower bound from 12 to 13 would've been enough - 0.13 already uses hex-conservative. But maybe it requires certain version to work so in that case we would need:

[features]
hashes = ["dep:hashes", "dep:hex-conservative"]

[dependencies]
hex-conservative = { version = ">=0.?", optional = true }
hashes = { package = "bitcoin_hashes", version = ">= 0.13, <= 0.14", default-features = false, optional = true }

Where ? in the version would be replaced with the one that had truncating implemented already. Note however that ranged version was broken by #723 so we have to fix it too if we want ranged versions.

But also, we have hex encoding/decoding in the crate anyway, so why not just have it directly as a dependency and use it for all formatting/decoding? Maybe optional with it being off making Debug slower and Display/FromStr not present.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, this is not why I've requested changes, you don't have to do it. It's just the wrong formatting that's a problem.

src/secret.rs Outdated
@@ -28,7 +28,7 @@ macro_rules! impl_display_secret {
hasher.write(&self.secret_bytes());
let hash = hasher.finish();

f.debug_tuple(stringify!($thing)).field(&format_args!("#{:016x}", hash)).finish()
f.debug_tuple(stringify!($thing)).field(&format_args!("#{:.16x}", hash)).finish()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes zero-padding since the type is just u64. Also the commit message is wrong - the type returned from finish is u64, not Hasher. We should just drop this commit.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sweat, dropped.

src/key.rs Outdated
#[cfg(feature = "std")]
{
let got = format!("{:?}", sk);
let want = "SecretKey(#2f4d520bd6874292)";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just realized, I don't believe this value is stable - different compiler version/OSes/... may produce something different. We need to check these properties instead:

got.len() == im_too_lazy_to_count_it;
got.starts_with("SecretKey(#");
got.ends_with(")");
got[11..(got.len() - 1)].chars().all(|c| c.is_ascii_hexdigit());
!sk.display_secret().to_string().contains(&got[11..(got.len() - 1)]);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is related. You've added a clearly broken test and the only reason it didn't manifest as broken is that nobody changed the hash algo between 1.63 and 1.80. The doc specifically says: "The internal algorithm is not specified, and so it and its hashes should not be relied upon over releases."

If you don't want to property-test it then please at least remove it. (with a comment that's why) Also it might be prudent to use RandomState::new().build_hasher() at least in tests to make sure nobody adds the test back by accident but potentially to also frustrate an attacker that would like to somehow recover some bits of the private key. (Does it make sense @apoelstra ?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh woops I can't read diffs, I was thinking that because I did not touch the std behaviour I did not write the test. My bad. I dropped the test, I think we are all convinced that this PR does what it says it does, re truncation. (I was very testy [0] yesterday afternoon.)

[0] definition of 'testy': https://duckduckgo.com/?q=testy+def&t=canonical&ia=web

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kixunil I think, ultimately Rust's reduced-siphash would have to be super broken for it to actually leak any bits of the key, and in the limit it can only leak 64 bits because that's how big its output is, but it does make me a little nervous to treat it as a random oracle like this.

Using a randomized hasher would help with this, at the cost of making the "key fingerprints" no longer work as fingerprints when they occur in logs. I think that would be super frustrating and likely even confusing, so we shouldn't do it in non-test contexts. (And yes, I appreciate that users already shouldn't be expecting these "fingerprints" to stay consistent because stdlib might change them....but in practice they probably will.)

Honestly I'm inclined to get rid of the Hasher stuff entirely and just put a <secret key; please enable hashes in rust-secp to see a fingerprint> type message in its place, just as we do for nostd users. I definitely wouldn't have written code that uses siphash like this and it looks like it originates in a time when I was much less strict in reviewed code than I would be with my own.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But will ACK/merge this since it's a strict improvement on the status quo.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good points, just note that the output of Debug is normally not considered stable.

just put a <secret key; please enable hashes in rust-secp to see a fingerprint> type message in its place

That sounds great to me! Let's do it.

Copy link
Collaborator

@Kixunil Kixunil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 3d1ce0d

Copy link
Member

@apoelstra apoelstra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 3d1ce0d successfully ran local tests

@apoelstra apoelstra merged commit 5d2149f into rust-bitcoin:master Aug 25, 2024
19 of 20 checks passed
@tcharding tcharding deleted the 08-22-truncate-secret-hash branch August 29, 2024 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants