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

Obfuscate shared secret when printing #396

Merged

Conversation

tcharding
Copy link
Member

@tcharding tcharding commented Feb 9, 2022

Currently printing the SharedSecret using Display or Debug prints the real secret, this is sub-optimal. We have a solution for other secrets in the project where printing is obfuscated and we provide a display_secret method for explicitly printing.

Mirror the logic for other secrets and obfuscate the SharedSecret when printing.

  • Patches 1 - 5: Clean up.
  • Patch 6: The meat and potatoes.

This is the final change needed to:
Resolve: #226

@tcharding tcharding force-pushed the obfuscate-debug-shared-secret branch from d12edb6 to de3527f Compare February 9, 2022 06:42
@tcharding
Copy link
Member Author

tcharding commented Feb 9, 2022

I'm confused as to why CI is failing? This PR does not make material changes to the 256 byte array so why are PartialEq etc. missing for Rust 1.29?

@tcharding tcharding force-pushed the obfuscate-debug-shared-secret branch from de3527f to 04726d9 Compare February 9, 2022 06:58
src/ecdh.rs Outdated
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut slice = [0u8; BUF_SIZE * 2];
let hex = to_hex(&self.secret, &mut slice).expect("fixed-size hex serializer failed");
Copy link
Member

Choose a reason for hiding this comment

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

This serializes the full buffer, which can be longer than the actual secret.
Maybe DisplaySecret should wrap the SharedSecret type and use its Deref impl to get the full secret?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think storing reference is better, see below.

Copy link
Member Author

Choose a reason for hiding this comment

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

Now that this PR is being done on top of #402, these comments are resolved I believe.

src/ecdh.rs Outdated Show resolved Hide resolved
@@ -91,7 +91,7 @@ pub struct DisplaySecret {
impl fmt::Debug for DisplaySecret {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unrelated but I think this should be called just Display - same as std::path::Display

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going to leave this as is for now, can re-visit if you feel strongly.

src/ecdh.rs Outdated
/// Formats the explicit byte value of the secret as a little-endian hexadecimal string using the
/// provided formatter.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DisplaySecret {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Name could be Display but ecdh::Display is weird. ecdh::shared_secret::Display would look better but since secret is the only type here the additional module looks weird. IDK what's best.

Copy link
Member Author

Choose a reason for hiding this comment

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

As above, I'll leave this for now. Thanks.

src/ecdh.rs Outdated Show resolved Hide resolved
src/ecdh.rs Outdated Show resolved Hide resolved
@tcharding
Copy link
Member Author

Converting to draft until #402 is merged or closed since 402 will effect how this PR (and review suggestions) are implemented.

@tcharding tcharding marked this pull request as draft February 10, 2022 10:40
@tcharding
Copy link
Member Author

You can't derive these because const generics are unsupported in older Rust versions and arrays have the traits implemented only up to size of 32.

Cool, thanks. That explains the mysterious (to me) Rust 1.29 CI fail.

@dr-orlovsky
Copy link
Contributor

JFYI previous work on a similar matter: #312

@tcharding
Copy link
Member Author

JFYI previous work on a similar matter: #312

Yep, I tying in with what you did there! Thanks

@tcharding tcharding force-pushed the obfuscate-debug-shared-secret branch 2 times, most recently from f7b229e to 8535fa1 Compare February 18, 2022 12:15
@tcharding
Copy link
Member Author

Strange that nightly and stable CI runs pass but not the others (its a rustdoc test that fails, should not be effected by the toolchain)?

@tcharding tcharding marked this pull request as ready for review February 18, 2022 12:25
@@ -35,7 +36,7 @@ macro_rules! impl_display_secret {

hasher.write(DEBUG_HASH_TAG);
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 the test fails because DefaultHasher::new() above returns hasher seeded with a different random seed each time. Which also means this implementation is actually wrong if we want to allow comparing different secrets.

Copy link
Member Author

Choose a reason for hiding this comment

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

Interesting, thanks for debugging it for me. So shared_secret_point is non-trivial to use then. I'll put some more work into it tomorrow. Cheers.

Copy link
Member Author

Choose a reason for hiding this comment

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

On further thought, I don't get that, the same data hashed multiple times should give the same output irrespective of the hashing implementation. I haven't looked at the code yet, I was just pondering your message.

Copy link
Collaborator

@Kixunil Kixunil Feb 19, 2022

Choose a reason for hiding this comment

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

Edit: something doesn't seem right about this, DefaultHasher::new is guaranteed to return same hasher and the implementation uses fixed seed 0, 0.

Original message:

std hasher is one intended for HashMap and implements DoS protection by randomizing among other things. It produces same hash for same input for same instances of the hasher but since we call new on each debug we're randomizing it.

if we want to allow comparing different secrets

I meant humans comparing, not computers here. Only the Debug implementation is wrong.
Unless we want to implement our own hasher or store DefaultHasher in static variable I suggest to just remove the hash when compiled without bitcoin_hashes.

src/secret.rs Outdated
/// let key = secp256k1::ONE_KEY;
///
/// // Normal display hides value.
/// assert_eq!("SecretKey(#2518682f7819fb2d)", format!("{:?}", key));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I see now, this is actually hard coded. The test is wrong because std doesn't guarantee any particular hash function or value. I suggest to test it with bitcoin_hashes only.

Copy link
Member

Choose a reason for hiding this comment

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

I think std should guarantee that the shared secrets don't change, and it is a breaking change if they do.

Having said this, we shouldn't guarantee the debug output of anything.

Copy link
Collaborator

@Kixunil Kixunil Feb 20, 2022

Choose a reason for hiding this comment

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

They don't guarantee it in case a better hash function is available so it can be switched. Notice that it's specifically called DefaultHasher - there was SipHasher before. I believe that's fine, if people want a specific hash function they should just explicitly use it (as a crate or NIH).

Agree on not guaranteeing debug output.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Happened to be the same hasher version that produced the expected output. So different versions of Rust produce different hashes - as documented.

Copy link
Member Author

Choose a reason for hiding this comment

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

Left an example of debug output in the rustdocs of SecretKey, removed the assertion. Removed all mention of debug output for the other two secrets (KeyPair and SharedSecret).

Copy link
Member

Choose a reason for hiding this comment

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

They don't guarantee it in case a better hash function is available so it can be switched. Notice that it's specifically called DefaultHasher - there was SipHasher before. I believe that's fine, if people want a specific hash function they should just explicitly use it (as a crate or NIH).

I really don't like this. The implication that if users try to share a secret, and one user has a slightly different version of rust-secp than the other, that they will be unable to communicate because their crypto doesn't work.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's non-issue because we only use this for debug output which must not be relied on. Creation of SharedSecret still uses sha256.

@tcharding tcharding force-pushed the obfuscate-debug-shared-secret branch 5 times, most recently from 5db4115 to 85fc336 Compare February 21, 2022 13:51
src/key.rs Outdated
#[inline]
pub fn serialize_secret(&self) -> [u8; constants::SECRET_KEY_SIZE] {
pub fn into_secret(&self) -> [u8; constants::SECRET_KEY_SIZE] {
Copy link
Member

Choose a reason for hiding this comment

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

In 2ccd54b:

I don't know about this. into_secret I would expect to produce a SecretKey or maybe a SharedSecret, not a byte array. A byte array I'd expect to get from something whose name was serialization-related.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Something bothered me about this and now I see what. secret.into_secret() is weird. Even worse it takes &self, which shouldn't use into_ prefix. I think the name should stay or _secret should be removed. Or perhaps call it to_bytes() to remove the impression that there's costly serialization.

Copy link
Member

Choose a reason for hiding this comment

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

Nice. I'd be happy with to_bytes.

Copy link
Member Author

Choose a reason for hiding this comment

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

The system works, to_bytes it is.

Copy link
Member Author

Choose a reason for hiding this comment

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

Funny side note, while doing this change originally I looked up that naming table (again), and managed to read it incorrectly and write the commit log based on this :) Naming things really is hard.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to write our own, non-confusing version. :)

apoelstra
apoelstra previously approved these changes Feb 22, 2022
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 ff39f28

This is a breaking change because it removes a bunch of the SharedSecret API. But I also contend that it's a breaking change because it changes the sharedsecret hash function.

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 22, 2022

@apoelstra did you confuse it with the other PR?

@apoelstra
Copy link
Member

My comment refers to 41dc861 which I think is only in this PR.

@apoelstra
Copy link
Member

Ah, no -- the first three commits are from an old version of #402, which is where the breaking changes come in.

@tcharding
Copy link
Member Author

tcharding commented Feb 23, 2022

My bad, this often happens to me, at first splitting the PRs seems like a good idea but if/as they grow it becomes entangled. And rebasing these was a PITA :)

@apoelstra
Copy link
Member

@tcharding is your intent here to build on #402?

@tcharding tcharding force-pushed the obfuscate-debug-shared-secret branch 2 times, most recently from ea6a378 to ea20bbb Compare February 24, 2022 08:42
@tcharding
Copy link
Member Author

tcharding commented Feb 24, 2022

Builds on top of the correct current tip of #402 now, thanks for noticing.

apoelstra added a commit that referenced this pull request Feb 24, 2022
5603d71 Limit SharedSecret to 32 byte buffer (Tobin Harding)
d5eeb09 Use more intuitive local var numbering (Tobin Harding)
834f63c Separate new_with_hash into public function (Tobin Harding)

Pull request description:

  Currently `SharedSecret` provides a way to get a shared secret using SHA256 _as well as_ a way to use a custom hash function to get the shared secret. Internally `SharedSecret` uses a 256 byte buffer, this is a tad wasteful. We would like to keep the current functionality but reduce memory usage.

  - Patch 1: Pulls the `new_with_hash` logic out into a standalone public function that just returns the 64 bytes representing the x,y co-ordinates of the computed shared secret point. Callers are then responsible for hashing this point to get the shared secret (idea by @Kixunil, thanks).
  - Patch 2: Does trivial refactor
  - Patch 3: Uses a 32 byte buffer internally for `SharedSecret`. This is basically a revert of the work @elichai did to add the custom hashing logic. @elichai please holla if you are not happy with me walking all over this code :)

  ### Note to reviewers

  Secret obfuscation is done on top of this in #396, they could be reviewed in order if this work is of interest to you.

ACKs for top commit:
  apoelstra:
    ACK 5603d71

Tree-SHA512: 48982a4a6a700a111e4c1d5d21d62503d34f433d8cb303d11ff018d2f2be2467fa806107018db16b6d0fcc5ff1a0325dd5790c62c47831c7cd2141a1b6f9467d
@apoelstra
Copy link
Member

#402 is merged. Can you rebase this? (It actually looks like the tip of 402 is still different from the commit in this PR)

Hashing the debug output for secrets can be done with `bitcoin_hashes`
not just `std`. Mention this in the obfuscated string output when
neither are available.
In array initialisation we use magic number 64, this is the secret bytes
length multiplied by 2.

Please note; we still use the magic number 32, left as such because it
is used in various ways and its not immediately clear that using a
single const would be any more descriptive.

Use `SECRET_KEY_SIZE * 2` instead of magic number 64.
The identifier `i` is predominantly used for indexing an array but we
are using it as a place holder for the iterated value of an array that
is then printed. The identifier `byte` is more descriptive.

Done in preparation for adding similar code to the `ecdh` module.
@tcharding
Copy link
Member Author

It actually looks like the tip of 402 is still different from the commit in this PR

It is hard to get good help :)

src/key.rs Outdated
@@ -212,9 +212,9 @@ impl SecretKey {
SecretKey(sk)
}

/// Serializes the secret key as byte value.
/// Gets the secret key as a byte value.
Copy link
Collaborator

Choose a reason for hiding this comment

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

"Returns" would sound more natural to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, use as suggested.

src/key.rs Outdated
#[inline]
pub fn serialize_secret(&self) -> [u8; constants::SECRET_KEY_SIZE] {
pub fn to_bytes(&self) -> [u8; constants::SECRET_KEY_SIZE] {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since this is key pair I think it'd be clearer to name this method secret_bytes() (Was also thinking of to_ prefix but that usually isn't used for getters.)

Copy link
Member

Choose a reason for hiding this comment

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

Weak ACK -- I think it would be clearer to label this method with secret somehow.

Copy link
Member Author

Choose a reason for hiding this comment

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

Too trigger happy with the the to_ conversions huh :) Use secret_bytes as suggested. Cheers.

The `serialize_secret` method is a getter method, it does not do any
serialisation. However we use the method on secret keys and key types so
in order for the name to be uniform use the descriptive name
`secret_bytes`.

Rename `serialize_secret` to be `secret_bytes`.
Improve rustdocs on `display_secret` by doing:

- Minor improvements to the rustdocs to aid readability in the editor.
- Do not guarantee (`assert_eq!`) debug output
Currently printing the `SharedSecret` using `Display` or `Debug` prints
the real secret, this is sub-optimal. We have a solution for other
secrets in the project where printing is obfuscated and we provide a
`display_secret` method for explicitly printing.

Mirror the logic for other secrets and obfuscate the `SharedSecret` when printing.
@tcharding
Copy link
Member Author

Implemented suggestions and force push. The only changes to the PR are the two suggested (docs fix and method name).

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 cf6badf

@apoelstra apoelstra merged commit ab6df6f into rust-bitcoin:master Feb 28, 2022
@tcharding tcharding deleted the obfuscate-debug-shared-secret branch March 1, 2022 16:21
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.

Don't let Debug or Display accidentally log SecretKeys
5 participants