Skip to content

Conversation

Sjors
Copy link
Member

@Sjors Sjors commented Jan 23, 2024

In #28983 I need to read and write two private keys to/from disk that are used by Stratum v2 peers to (optionally) authenticate us.

For the write part, I initially just put the key data into a std::vector<unsigned char> and then used a modified version of WriteBinaryFile. But @vasild pointed out in #29229 that:

CKey stores sensitive data and takes care to wipe it from memory when freed. In #28983 Read/WriteBinaryData() is used in a way that defeats that - the sensitive data will be copied to a temporary variable (ordinary std::vector) for IO. Can we change Read/WriteBinaryData() to operate directly on CKey in such a way that data goes directly from CKey to the disk?

This PR tries a different approach that hopefully addresses that. See Sjors#32 for how it's used (in sv2_template_provider.cpp).

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 23, 2024

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
Stale ACK shaavan, vasild

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #29675 (wallet: Be able to receive and spend inputs involving MuSig2 aggregate keys by achow101)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@Sjors
Copy link
Member Author

Sjors commented Jan 23, 2024

Opened as draft because I suspect this can be done less verbosely.

@Sjors
Copy link
Member Author

Sjors commented Jan 23, 2024

Added Serialize for completeness. This only saves me a MakeUCharSpan(key).

@Sjors Sjors changed the title CKey: add Unserialize CKey: add Serialize and Unserialize Jan 23, 2024
@Sjors Sjors mentioned this pull request Jan 23, 2024
Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

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

Concept ACK

src/key.h Outdated
template<typename Stream>
void Serialize(Stream &s) const {
if (!fCompressed) {
throw std::ios_base::failure("Uncompressed key");
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it not worth saving CKey::fCompressed to disk as well and fully support ser/unser of any CKey?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point.

@Sjors
Copy link
Member Author

Sjors commented Jan 25, 2024

Switched to the approach suggested by @vasild. Also supports uncompressed keys. Added test.

Keeping this draft pending #29307.

@Sjors Sjors force-pushed the 2024/01/ckey_unserialize branch from 84ac6c3 to 8c067ef Compare January 25, 2024 14:16
@Sjors Sjors marked this pull request as ready for review January 25, 2024 14:17
@Sjors
Copy link
Member Author

Sjors commented Jan 25, 2024

Come to think of it, this PR only changes serialization, while #29307 deals with storage. So this is ready for review.

Copy link
Contributor

@shaavan shaavan left a comment

Choose a reason for hiding this comment

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

Code Review ACK 8c067ef

Notes:

  • The Unserialise function first reads the compression flag from the stream and then updates the actual keydata.
  • The Serialise function first writes the compression flag, followed by the keydata
  • The test appropriately verifies the added code for both the compressed and uncompressed keys.

@DrahtBot DrahtBot requested a review from vasild January 26, 2024 14:06
Copy link
Member

@maflcko maflcko left a comment

Choose a reason for hiding this comment

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

How is this different from CPrivKey?

src/key.h Outdated
inline void Unserialize(Stream& s) {
s >> fCompressed;
MakeKeyData();
s >> MakeWritableByteSpan(*keydata);
Copy link
Member

Choose a reason for hiding this comment

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

Not sure. IIUC the assumption is that keydata should not hold invalid keys. Can you explain why this assumption should be broken?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do CKey::Check() on keydata[0]?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@josibake
Copy link
Member

I need to read and write two private keys to/from disk

Not familiar with the stratumv2 spec, but this seems odd to me and also not a good fit for CKey. FWIW, there is CPrivKey for producing a serialized OpenSSL private key

@Sjors
Copy link
Member Author

Sjors commented Jan 29, 2024

I added Check() when deserialising.

Not familiar with the stratumv2 spec, but this seems odd to me

In Stratum v2 a pool or Template Provider (implemented in #28983) may (optionally) have an identity. This is necessary to prevent stealing hash rate (at least for the pool). In order to protect that identity in the long run, you can keep its private key on a different machine, generate a static key for the server and sign a certificate for that static key.

not a good fit for CKey

Why would you not use a CKey to hold a private key?

FWIW, there is CPrivKey for producing a serialized OpenSSL private key

TIL. There's two issues with CPrivKey:

  1. It's designed to always be used in conjunction with a public key. That makes sense for the wallet which has to deal with encrypted key, and where any data corruption is catastrophic.

With Stratum v2 you can just make a new static key if something goes wrong. It's more similar to how we handle Tor v3 and I2P private keys.

It's possible to use the confusingly named GetPrivKey() instead of <<, and it's possible to Load() with a dummy public key and fSkipCheck = false. But having straight-forward << and >> seems cleaner.

  1. It uses the needlessly large OpenSSL encoding. At least in Stratum v2 there's never a need to import or export private keys. We just need to store them. The encoding is also incorrect (for Stratum v2): it just pretends to be a compressed key, but in reality public keys are encoded as x-only. There's no point in shoe-horning data into a standard we don't need and use incorrectly. On the other hand, there's no harm in it - the wallet does it too.

I could switch to OpenSSL encoding. Either by moving the code from GetPrivKey to Serialize(), or vice versa, call GetPrivKey() from Serialize. Load would need a more involved refactor to separate out the dependency.

@vasild wrote earlier:

Is it not worth saving CKey::fCompressed to disk as well and fully support ser/unser of any CKey?

I added this before, but given the existence of CPrivKey I'm tempted to drop this. That makes it more clear that is only for keys that are intended to be used with Schnorr signatures, x-only ECDH, etc. I.e. keys for which there is no valid DER encoding.

@Sjors Sjors force-pushed the 2024/01/ckey_unserialize branch from 8c067ef to 683f988 Compare January 29, 2024 10:41
Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

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

ACK 683f988

@DrahtBot DrahtBot requested review from shaavan and removed request for shaavan January 29, 2024 10:52
@Sjors Sjors force-pushed the 2024/01/ckey_unserialize branch from 683f988 to 3f11c15 Compare January 29, 2024 11:13
@Sjors
Copy link
Member Author

Sjors commented Jan 29, 2024

I dropped support for uncompressed keys and added a comment to explain the difference with OpenSSL encoding. Also added test for invalid keys.

@Sjors Sjors force-pushed the 2024/01/ckey_unserialize branch from 3f11c15 to 0f5d0a6 Compare January 29, 2024 11:14
src/key.h Outdated
}

template <typename Stream>
inline void Unserialize(Stream& s) {
Copy link
Member

Choose a reason for hiding this comment

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

template implies inline, so it can be removed. Also, could run clang-format on new code?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done. Would be nice to do this in a pre-commit hook or something, because it's unlikely I'm going to remember doing this every time.

src/key.h Outdated
// Uncompressed keys should be OpenSSL serialized instead
throw std::ios_base::failure("uncompressed key");
}
::Serialize(s, MakeUCharSpan(*this));
Copy link
Member

@maflcko maflcko Jan 29, 2024

Choose a reason for hiding this comment

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

Any reason to cast a byte span to a u-char span? I think you can replace all Make*Span() by just Span{}.

s << Span{*this};

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in both places.

@DrahtBot DrahtBot requested review from shaavan and removed request for shaavan March 7, 2024 09:25
@josibake
Copy link
Member

josibake commented Mar 7, 2024

others copy-paste my example in stead of repeating my original mistake

I really don't expect serializing and unserializing keys to disk (outside of the existing wallet use case) to be a common pattern. As @ryanofsky mentioned, I think it's good to have a little friction here to make the implementer think "should I really be doing this?"

This seems an overkill way to avoid adding a standard serialization method.

I don't think it's fair to claim you are adding a standard method here. As I mentioned before:

I'd be more inclined if this PR was proposing a general refactor that got rid of existing 'needlessly complicated ways' of serializing/unserializing, rather than just adding yet another method. That way, we end up with a well documented and standard way of doing things.

But this PR is only adding yet another method, which is unnecessary considering your use case can be accomplished with the existing methods.

@DrahtBot DrahtBot requested review from shaavan and removed request for shaavan March 7, 2024 09:43
@Sjors
Copy link
Member Author

Sjors commented Mar 7, 2024

I don't think it's fair to claim you are adding a standard method here.

By standard, I mean the same way we serialise other objects.

I do agree it's not ideal to have multiple serialisation standards for the same object. A further refactor could address that, e.g. in a way similar to how we can serialize a CTransaction with or without witness (TX_WITH_WITNESS / TX_NO_WITNESS).

@DrahtBot DrahtBot requested review from shaavan and removed request for shaavan March 7, 2024 09:54
@josibake
Copy link
Member

josibake commented Mar 7, 2024

A further refactor could address that

I think a better approach would be to use the existing methods for your use case and then if we start seeing more use cases pop up where people absolutely need to serialize and unserialize raw CKeys to disk, we do a general refactor of CKey that proposes standard methods for serializing/unserializing and removes the old ways of doing it.

@DrahtBot DrahtBot requested review from shaavan and removed request for shaavan March 7, 2024 10:10
@Sjors
Copy link
Member Author

Sjors commented Mar 7, 2024

I think a better approach would be to use the existing methods for your use case

As discussed a further above (#29295 (comment)) there's quite a few other things I need to get merged for Stratum v2 before this PR becomes a potential blocker. It's only at that point that I'll try other approaches.

I'll mark this as draft for now.

Co-authored-by: Vasil Dimov <vd@FreeBSD.org>
@Sjors Sjors force-pushed the 2024/01/ckey_unserialize branch from 57fbd6c to 8d35a9b Compare August 8, 2024 16:26
@Sjors Sjors closed this Oct 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants