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

encapsulate signer #3576

Merged
merged 15 commits into from
Dec 9, 2024
Merged

encapsulate signer #3576

merged 15 commits into from
Dec 9, 2024

Conversation

richardpringle
Copy link
Contributor

@richardpringle richardpringle commented Nov 27, 2024

Depends on ava-labs/coreth#693. But ava-labs/coreth#693 also depends on this. Not sure about merge order...

Why this should be merged

Currently, signing happens all over the place and should definitely be abstracted away, especially when dealing with sensitive data like a private key.

Once this abstraction lands, we could further enhance security by using secure hardware and signing outside of the avalanchego application and potentially integrate platforms like cubist.

How this works

Encapsulate signing such that the secret-key does not leave our bls utility package.
The main changes are in this file: utils/crypto/bls/secret.go

How this was tested

CI

Need to be documented in RELEASES.md?

I don't think so?

@richardpringle richardpringle force-pushed the encapsulate-signer branch 2 times, most recently from c522086 to fc6bc8d Compare November 27, 2024 23:30
go.mod Outdated
@@ -190,3 +190,5 @@ require (
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

replace github.com/ava-labs/coreth v0.13.9-rc.1 => github.com/ava-labs/coreth v0.13.9-rc.1.0.20241127225719-c225e43551e1
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure what the process is here

@@ -128,7 +128,7 @@ type Config struct {
// TLSKey is this node's TLS key that is used to sign IPs.
TLSKey crypto.Signer `json:"-"`
// BLSKey is this node's BLS key that is used to sign IPs.
BLSKey *bls.SecretKey `json:"-"`
BLSKey bls.Signer `json:"-"`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I assume this means the BLSKey property gets serialized as the string, "-"?

Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC this actually omits it from serialization, i.e it's not serialized or deserialized. This is done because we use the Config struct to also hold fields and not just config values.... so our hack is to ignore serialization for non-config values (which is weird IMO).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense.

Yeah, I generally prefer to split up raw config data vs actual dependent services/structs, but 🤷. I've seen this pattern before too.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yup think it makes sense to leave this as is in this PR. Separating it would be an improvement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

one day...

@@ -186,7 +186,7 @@ type ManagerConfig struct {
SybilProtectionEnabled bool
StakingTLSSigner crypto.Signer
StakingTLSCert *staking.Certificate
StakingBLSKey *bls.SecretKey
StakingBLSKey bls.Signer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we want to change the property name here too?

@@ -694,7 +694,7 @@ func getStakingSigner(v *viper.Viper) (*bls.SecretKey, error) {
return nil, fmt.Errorf("couldn't create path for signing key at %s: %w", signingKeyPath, err)
}

keyBytes := bls.SecretKeyToBytes(key)
keyBytes := key.ToBytes()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll have to change this eventually to fully abstract the signer. I'm open to ideas on how best to do this

Copy link
Collaborator

Choose a reason for hiding this comment

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

Within the config, I'd expect us to have a switch that offers different BLS Signers ie. generate and store a new BLS key on the machine or use an external signing service.

I'd think this is either AvalancheGo defining the options it wants to support directly here or it supports a gRPC service that dials an external signer and handles signature requests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, looking at the code again, signer creation would be specific. A LocalSigner should actually write the key to disk before returning from a constructor. We could have an EphemeralSigner that's similar but doesn't save the key and is used for testing.

With that said, I would probably create a

func initializeSigner(signerConfig) (Signer) { /* ... */ }

and deal with saving or not saving there. It should not be dealt with here IMO.

This is all for a future PR though

@@ -175,7 +175,7 @@ func newTestNetwork(t *testing.T, count int) (*testDialer, []*testListener, []id
require.NoError(t, err)
nodeID := ids.NodeIDFromCert(cert)

blsKey, err := bls.NewSecretKey()
blsKey, err := bls.NewSigner()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤔 If we want to change all the *Key names (variables or properties), I would recommend/request that I do that in a follow-up to limit the scope of both changes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems reasonable

@yacovm
Copy link
Contributor

yacovm commented Nov 28, 2024

Once this abstraction lands, we could further enhance security by using secure hardware and signing outside of the avalanchego application and potentially integrate platforms like cubist.

Do they support BLS? Most hardware security modules only support ECDSA / ed25519...

@richardpringle
Copy link
Contributor Author

Once this abstraction lands, we could further enhance security by using secure hardware and signing outside of the avalanchego application and potentially integrate platforms like cubist.

Do they support BLS? Most hardware security modules only support ECDSA / ed25519...

Yeah, they specifically have support for Ethereum validators

@@ -25,49 +25,64 @@ var (

type SecretKey = blst.SecretKey
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I should get rid of this type

Copy link
Contributor

@marun marun left a comment

Choose a reason for hiding this comment

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

tests/ changes LGTM

snow/context.go Outdated Show resolved Hide resolved
utils/crypto/bls/secret.go Outdated Show resolved Hide resolved
utils/crypto/bls/secret.go Show resolved Hide resolved
// NewSecretKey generates a new secret key from the local source of
// cryptographically secure randomness.
func NewSecretKey() (*SecretKey, error) {
func NewSigner() (Signer, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: export signer and return it as a concrete type here so that the calling code can decide whether to use it as an interface/as a concrete type. We'll have to figure out a fix/good naming scheme since it'll collide w/ the interface name as it currently is though. We could also as an alternative argue that the interface is premature and just use the struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🤔 I don't think the interface is premature though, I don't plan on adding any functionality for the other implementations. What I'm trying to get is encapsulation so that you can't access the secret-key outside of this package. I think want to make a LocalSigner, then some "cloud-based" signers (there are a couple of options here that will each have their own concrete types).

Having it generic from day 1 minimizes the impact of converting what's here to a LocalSigner (which would also be used in all tests), and other signer types would be created based on config values on application start.

Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Discussed offline. Change signer to LocalSigner. Will move this out in a follow PR

Copy link
Contributor

@joshua-kim joshua-kim left a comment

Choose a reason for hiding this comment

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

Another idea is to cache the key bytes and public key but not going to block on it since it's an increase in scope.

go.mod Outdated Show resolved Hide resolved
Signed-off-by: Richard Pringle <[email protected]>
Copy link
Collaborator

@aaronbuchwald aaronbuchwald left a comment

Choose a reason for hiding this comment

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

Could we update to a coreth rc rather than a commit before merging to master? After that, this LGTM

@@ -128,7 +128,7 @@ type Config struct {
// TLSKey is this node's TLS key that is used to sign IPs.
TLSKey crypto.Signer `json:"-"`
// BLSKey is this node's BLS key that is used to sign IPs.
BLSKey *bls.SecretKey `json:"-"`
BLSKey bls.Signer `json:"-"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yup think it makes sense to leave this as is in this PR. Separating it would be an improvement

@@ -175,7 +175,7 @@ func newTestNetwork(t *testing.T, count int) (*testDialer, []*testListener, []id
require.NoError(t, err)
nodeID := ids.NodeIDFromCert(cert)

blsKey, err := bls.NewSecretKey()
blsKey, err := bls.NewSigner()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems reasonable

go.mod Outdated
@@ -10,7 +10,7 @@ require (
github.com/DataDog/zstd v1.5.2
github.com/NYTimes/gziphandler v1.1.1
github.com/antithesishq/antithesis-sdk-go v0.3.8
github.com/ava-labs/coreth v0.13.9-rc.1
github.com/ava-labs/coreth v0.13.9-rc.1.0.20241204232641-e73e655356b7
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we create an rc with a name that indicates what the change is for (ref to previous update of coreth dependency 33bd401 ) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -694,7 +694,7 @@ func getStakingSigner(v *viper.Viper) (*bls.SecretKey, error) {
return nil, fmt.Errorf("couldn't create path for signing key at %s: %w", signingKeyPath, err)
}

keyBytes := bls.SecretKeyToBytes(key)
keyBytes := key.ToBytes()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Within the config, I'd expect us to have a switch that offers different BLS Signers ie. generate and store a new BLS key on the machine or use an external signing service.

I'd think this is either AvalancheGo defining the options it wants to support directly here or it supports a gRPC service that dials an external signer and handles signature requests.

}

// SecretKeyFromBytes parses the big-endian format of the secret key into a
// secret key.
func SecretKeyFromBytes(skBytes []byte) (*SecretKey, error) {
func SecretKeyFromBytes(skBytes []byte) (Signer, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
func SecretKeyFromBytes(skBytes []byte) (Signer, error) {
func SecretKeyFromBytes(skBytes []byte) (*LocalSigner, error) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done fd7f326

Comment on lines 33 to 34
// TODO: delete me
ToBytes() []byte
Copy link
Contributor

Choose a reason for hiding this comment

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

If we adopt the suggested change on SecretKeyFromBytes - then this can be deleted now.

Suggested change
// TODO: delete me
ToBytes() []byte

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done fd7f326

Comment on lines +31 to +32
Sign(msg []byte) *Signature
SignProofOfPossession(msg []byte) *Signature
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like these functions are going to need to return error once we want to use them for remote signing. Are we waiting to add error to the return value for when that other implementation is introduced?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't think there's any point in pre-maturely defining the interface here. It's unclear to me at this point whether or not we'll want the specific signer implementation to handle its own errors or wether we would want the caller to handle errors (I'm missing some details on how to gracefully shutdown).

I'm interested to hear your thoughts here, but I definitely don't think this is something that we should deal with yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good. I think the only complex question will be around the PoP in the networking code... Everything else should be fairly straightforward I think... But 🤷 we can deal with that when we need to I suppose.

@StephenButtolph StephenButtolph added this pull request to the merge queue Dec 9, 2024
Merged via the queue into master with commit 1dc4192 Dec 9, 2024
23 checks passed
@StephenButtolph StephenButtolph deleted the encapsulate-signer branch December 9, 2024 21:56
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.

6 participants