-
Notifications
You must be signed in to change notification settings - Fork 12
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
refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth #335
Conversation
…able with custom / new auth
@@ -79,33 +78,17 @@ func (pub *Secp256k1PublicKey) Verify(sig []byte, hash []byte) error { | |||
sig = sig[:len(sig)-1] | |||
} | |||
|
|||
if len(sig) != 64 { | |||
return errInvalidSignature | |||
if len(sig) != secp256k1SignatureLength { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also change the line above using 65
@@ -12,13 +15,13 @@ import ( | |||
|
|||
func TestSecp256k1PrivateKey_Sign(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should change the test name since you're not testing Sign
method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually should two separated test functions for Sign
and SignWithRecoveryID
err := pubKey.Verify(tt.sigBytes, hash) | ||
assert.ErrorIs(t, err, tt.wantErr) | ||
err := pubKey.Verify(tt.sigBytes, hash[:]) | ||
if tt.wantErr != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert.ErrorIs
should work here, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops
test/driver/client_driver.go
Outdated
@@ -126,7 +126,7 @@ func (d *KwildClientDriver) DropDatabase(ctx context.Context, dbName string) ([] | |||
return nil, fmt.Errorf("error dropping database: %w", err) | |||
} | |||
|
|||
d.logger.Info("drop database", zap.String("name", dbName), zap.String("owner", d.GetUserAddress()), | |||
d.logger.Info("drop database", zap.String("name", dbName), zap.Binary("owner", d.GetUserPublicKey()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will zap.Binary
will use HEX encoding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, will test it. If not, I will change it to fmt.Sprintf("%x", d.GetUserPublicKey())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It encodes as base64, which I think is fine
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we kind of agree on using HEX everywhere for PublicKey
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. Ok I'll go back and change that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So authenticator is only for verification, I'm not exactly sure what kind of different validation logic could be applied except what we included. I mean it seems only one way to 'correctly' verify the signature?
A observation is that we already verified TX or Message when it's received by a Kwil node, also in CheckTx
, in engine we verified again. This seems redundant, and also, because this verification logic is share by blockchain and engine, we kind allow 3rd part to change the concensus (although engine is also concensus).
Just a thought, would it be useful if we can allow another signature in the payload, and the smartcontract(e.g. kuneiform) could use separeted verification logic to verify. So we only give out the ability to do different verification in engine(or kuneiform), but not for Tx or Message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But then you need a separate wallet to forward messages to the blockchain, and actually pay the gas. That seems like a hack around the system, but if our most important users are hacking around a system this early in its product lifecycle, then it probably isn't made well.
The entire point of this is that other devs can implement their own logic easily. A great example is NEAR's new signer, which uses their own custom logic for the signing.
The authenticators used here are exactly what would be used in CheckTx
; if we implemented another and registered it, it would be globally available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized in js-sdk we allow developer pass a customized Signer, now this make sense to me, since developer can use their pair of signing and verifying, correct? I doesn't make sense to me to use our Signer and their customized Authenticator.
Yeah this another signature is weird, we don't need it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But still, I think verify once when Tx/Message is received from the node is enough? Not sure why we did multiple times
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't change the point in which signatures get checked. That is all consistent for what was before this PR and after. The only difference is how we separate the logic. Nowhere are there any extra signature checks that we did not do before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree, verified multiple times is not related to this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it seems that the only places that we verify transactions are when we receive them over gRPC and in CheckTx
, which is indeed redundant since we broadcast synchronously. We can probably take it out in a different PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, also, CallMessage
are verified in both gRPC and dataset module.
// constants for eth personal sign | ||
const ( | ||
// ethAuth is the authenticator name | ||
EthAuth = "secp256k1_ep" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The registration system makes sense, as does the Verify
method of the Authenticator interface
. Just thinking through usability a bit here...
These exported strings are used for the registered authenticator name, and they are the values set for Signature.Type
. Except in tests, only the signer implementations really have to know of these const values e.g. EthPersonalSigner
makes a sig with Type: EthAuth
.
The current (*Transaction).Verify
, as an example use case:
func (t *Transaction) Verify() error {
msg, err := t.Body.SerializeMsg(t.Serialization)
if err != nil {
return err
}
authenticator, err := auth.GetAuthenticator(t.Signature.Type)
if err != nil {
return err
}
return authenticator.Verify(t.Sender, msg, t.Signature.Signature)
}
I like that we don't have to trouble with crypto.PublicKeyFromBytes
any more, but it looks like because the Signature.Type
determines what verification code to use then a consumer like this Transaction
method does not have to be concerned with the authenticator registry or authenticators at all.
The type is pkg/auth.Signature
, same package as the registry, so :
pkg/auth/signer.go
func (sig *Signature) Verify(msg, pubkey []byte) error {
a, err := GetAuthenticator(sig.Type)
if err != nil {
return err
}
return a.Verify(pubkey, msg, sig.Signature)
}
pkg/transactions/transaction.go
func (t *Transaction) Verify() error {
msg, err := t.Body.SerializeMsg(t.Serialization)
if err != nil {
return err
}
return t.Signature.Verify(msg, t.Sender)
}
I think the win with the Authenticator interface and the driver registry is flexibility in expanding the authentication / verification capabilities of kwild, but I'm not sure much of the kwild code has to directly access the registry in the case of signature verification.
Still looking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah that makes a lot of sense. I'll make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It still is used for Address()
, particularly in the pkg/sql/sqlite/functions/addresses
package. I don't love this, but not quite sure of a workaround.
authenticator, err := auth.GetAuthenticator(ident.AuthType) | ||
if err != nil { | ||
return raiseErr(addressFuncName, fmt.Errorf("failed to get authenticator: %w", err)) | ||
} | ||
|
||
address, err := authenticator.Address(ident.PublicKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second general purpose of the Authenticator interface
is to provide an address from a pubkey. Starting to feel a bit like the Authenticator
concept is mainly of concern to the implementor of a new signature scheme, but for the kwild code, it's concern is with one function or the other.
So as with my comment on signature verification, do we trouble this caller with the Authenticator
interface or just let them do auth.Address(ident.PublicKey, ident.AuthType)
.
I'm looking at this wondering what is gained from having the public GetAuthenticator
function vs accessing the registry indirectly via the specific types and functions provided by the auth
package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that makes a lot more sense. Another area I was sort've hung up on was having Address
be tied to the Authenticator
. For the user of Kwil, I think it makes sense; people would use this to create a new way to authenticate users that use some existing standard, and that usually also involves a unique way of identifying those users.
Within kwild
, a bit of a different beast, so I'm not totally tied to this structure.
c4a072e
to
4ec61cb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, just some tiny nits. Haven't manually tested either, but acceptance tests are working.
@@ -0,0 +1,81 @@ | |||
package auth |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A package doc would be valuable in the future. Accepted format is // Package auth ...
just above package auth
. https://go.dev/doc/comment
pkg/auth/auth.go
Outdated
// ListAuthenticators returns a list of registered authenticators | ||
func ListAuthenticators() []struct { | ||
Name string | ||
Authenticator Authenticator | ||
} { | ||
var authenticators []struct { | ||
Name string | ||
Authenticator Authenticator | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can probably toast this function too now, but no objection if you like it for debugging or whatever.
pkg/client/client.go
Outdated
@@ -65,7 +66,7 @@ func Dial(target string, opts ...Option) (c *Client, err error) { | |||
zap.String("host", c.transportClient.GetTarget()), | |||
} | |||
if c.Signer != nil { | |||
zapFields = append(zapFields, zap.String("from", c.Signer.PubKey().Address().String())) | |||
zapFields = append(zapFields, zap.String("from", fmt.Sprintf("%x", c.Signer.PublicKey()))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I think I might have provided a slightly misleading tip about the %x
trick a little while ago. It's super handy for logging to embed a hex string for a []byte
in a longer message, but there is one kinda nasty gotcha to be aware of, which is that if the type upon which %x
is applied is also a Stringer
, it takes the very unexpected action of running the String
method first and then casting a la []byte(...)
before applying hex conversion again. In other words you can end up converting twice if the type happens to have a String
method.
For this reason, it's probably best to limit it's use when the value is from a function-local variable of type []byte
(not returned by a method of something from another package that could add a String method at any time breaking the log) and when the entirety of the msg is more than just %x
.
Sorry about that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a weird quirk. Noted
pkg/transactions/transaction.go
Outdated
// func (t *Transaction) GetSenderPubKey() (crypto.PublicKey, error) { | ||
// return crypto.PublicKeyFromBytes(t.Signature.KeyType(), t.Sender) | ||
// } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rm?
pkg/transactions/transaction.go
Outdated
@@ -121,7 +122,8 @@ func CreateTransaction(contents Payload, nonce uint64) (*Transaction, error) { | |||
type Transaction struct { | |||
// Signature is the signature of the transaction | |||
// It can be nil if the transaction is unsigned | |||
Signature *crypto.Signature | |||
// This should probably be renamed to "Authentication" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
meh, but I feel like signatures and transactions are a very tight knit pair and it would be confusing as "Authentication".
pkg/transactions/message_test.go
Outdated
// { | ||
// name: "non support message serialization type", | ||
// args: args{ | ||
// mst: transactions.SignedMsgSerializationType("non support message serialization type"), | ||
// signer: ðPersonalSigner, | ||
// }, | ||
// wantErr: true, | ||
// }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
temporarily commented?
pkg/engine/types/user.go
Outdated
// // UserIdentifier is an interface for identifying a user by public key | ||
// type UserIdentifier interface { | ||
// MarshalBinary() ([]byte, error) | ||
// PubKey() (crypto.PublicKey, error) | ||
// UnmarshalBinary(data []byte) error | ||
// Address() (string, error) | ||
// } | ||
|
||
type User struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine to rm the old UserIdentifier
imo, maybe also a short doc on User
.
…able with custom / new auth (#335) * refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth * added gavins changes * changed public key logging from base64 to hex * better abstracted away the authenticators, as suggested by Jon * added jons changed
…able with custom / new auth (#335) * refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth * added gavins changes * changed public key logging from base64 to hex * better abstracted away the authenticators, as suggested by Jon * added jons changed
…able with custom / new auth (#335) * refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth * added gavins changes * changed public key logging from base64 to hex * better abstracted away the authenticators, as suggested by Jon * added jons changed
…able with custom / new auth (#335) * refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth * added gavins changes * changed public key logging from base64 to hex * better abstracted away the authenticators, as suggested by Jon * added jons changed
…able with custom / new auth (#335) * refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth * added gavins changes * changed public key logging from base64 to hex * better abstracted away the authenticators, as suggested by Jon * added jons changed
…able with custom / new auth (#335) * refactored crypto, addresses, and the engines auth to be better pluggable with custom / new auth * added gavins changes * changed public key logging from base64 to hex * better abstracted away the authenticators, as suggested by Jon * added jons changed
This PR is meant to simplify our supported authentication types.
Overview
The tldr of this PR is that it better delineates (imo) the responsibilities of
pkg/crypto
,pkg/crypto/addresses
(now gone), and how we handle authentication in the engine. It introduces a new packagepkg/auth
, which handles the semantics of signature digests. The intent of this is to make for more "pluggable" auth, whether it be us, or another party implementing an auth driver (as discussed in Slack).What I think is pretty exciting about this is that it makes it trivially easy to implement many types of authentication. It would be dead simple to add, for example, RSA signatures into
kwild
(we don't actually want to do that, but it would be very easy).pkg/auth
I'm still not quite sure on this package name.
This package defines two very clear interfaces for signing and verification. The intent here is that users can implement their own signature verification, and we support a standard set of signing methods. To implement new signature verification logic, all a user needs to do is implement the following interface:
The user can then register that
Authenticator
with a unique id, which will then be registered as a driver. This works very similar to Go'sdatabase/sql
drivers.The
Signer
interface andSignature
struct frompkg/crypto
was moved here. This is because the specifics of the signature algorithm that we are using are higher level than whatpkg/crypto
handles now.pkg/crypto/addresses
was also moved intopkg/auth
, however ~70% was actually just deleted, since the system is quite a bit simpler and didn't need all of it.Changes to
pkg/crypto
This pr really simplifies
pkg/crypto
. Previously, we had support for all different types of signers implemented there, as well as logic to switch and validate the signatures generated by them. Now, we only implement basic secp256k1 and ed25519 keypairs there, and enforce any other logic inpkg/auth
.The one hangup I'm not quite sure on is go-ethereum's recovery ID. As far as I can tell, it is not technically "standard" secp256k1, but both Ethereum and Bitcoin use it, and it is so widely adopted that it is actually hard to find libraries that do not contain it. CometBFT, however, does not use it. I left it to
pkg/crypto
for now, but I feel like it probably should live inpkg/auth
.One of the other big changes in
pkg/crypto
is that we no longer have semantics likeKeyType
, thePrivateKey
interface, thePublicKey
interface, and theAddress
interface. This wasn't really intentional, but by the time I had made all of the other changes, they actually were just not needed anymore.Changes to
pkg/crypto/addresses
This entire package got deleted. All of the
KeyIdentifier
logic was totally removed, and the address derivation was moved topkg/auth
.Changes to the engine
Previously, the engine relied on
pkg/crypto/addresses/KeyIdentifier
for user authentication (within the master db, in order to unmarshal). I always knew this was weird, but due to the rush we were in previously, kept it. The engine now defines it's own type for identifying users.The one thing I really do not like still is that
pkg/sql/sqlite/functions/addresses
is very coupled to both the engine as well aspkg/auth
. This now makes me think that we should not actually supportpublic_key()
andaddress()
scalar functions, and instead we should revert to the@caller
and@caller_address
semantic. Obviously, this would require getting Fractal onboard with a slight breaking change, so I punted on it for now.Other misc changes
A lot of other miscellaneous changes were made, mostly due to how prevalent
pkg/crypto
is in our system. Some of these include:kwil-admin
andkwil-cli
, which relied on thepkg/crypto/PrivateKey
interface. This change was really minor, since we only support secp256k1 inkwil-cli
, and ed25519 inkwil-admin
.pkg/transactions
to use the newpkg/auth
.pkg/modules/dataset
. The new auth from bothpkg/auth
as well as the new way the engine identifies "users" necessitated an obvious change, but it does feel a lot cleaner. It also got rid of an unnecessary interface that decoupledpkg/transactions/CallMessage
. I felt this was justifiable since the module is heavily reliant onpkg/transactions/Transaction
, and the interface was really only used in once place and had one implementation. I still think something along those lines is probably appropriate, but it felt outside the scope of this PR, and if we are going to include it, then we should fully decouplepkg/transactions