mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
336 lines
9.6 KiB
Go
336 lines
9.6 KiB
Go
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"fmt"
|
|
|
|
"cosmossdk.io/errors"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
|
|
apiv1 "github.com/sonr-io/sonr/api/did/v1"
|
|
"github.com/sonr-io/sonr/x/did/types"
|
|
|
|
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
|
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
|
)
|
|
|
|
// VerifyWalletOwnership verifies that the provided signature proves ownership of the wallet
|
|
func (k Keeper) VerifyWalletOwnership(
|
|
ctx context.Context,
|
|
walletAddress, chainID string,
|
|
walletType types.WalletType,
|
|
challenge, signature []byte,
|
|
) error {
|
|
switch walletType {
|
|
case types.WalletTypeEthereum:
|
|
return k.verifyEthereumSignature(walletAddress, challenge, signature)
|
|
case types.WalletTypeCosmos:
|
|
return k.verifyCosmosSignature(ctx, walletAddress, challenge, signature)
|
|
default:
|
|
return errors.Wrapf(types.ErrUnsupportedWalletType, "wallet type: %s", walletType)
|
|
}
|
|
}
|
|
|
|
// verifyEthereumSignature verifies an Ethereum signature using ECDSA recovery
|
|
func (k Keeper) verifyEthereumSignature(walletAddress string, challenge, signature []byte) error {
|
|
// Validate Ethereum address format
|
|
if !common.IsHexAddress(walletAddress) {
|
|
return errors.Wrap(types.ErrInvalidEthereumAddress, "invalid address format")
|
|
}
|
|
|
|
// Convert address to common.Address
|
|
expectedAddr := common.HexToAddress(walletAddress)
|
|
|
|
// Ethereum uses personal_sign which prefixes the message
|
|
// The format is: "\x19Ethereum Signed Message:\n" + len(message) + message
|
|
prefixedMessage := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(challenge), challenge)
|
|
messageHash := crypto.Keccak256Hash([]byte(prefixedMessage))
|
|
|
|
// Recover the public key from the signature
|
|
// Ethereum signatures have a recovery parameter v at the end
|
|
if len(signature) != 65 {
|
|
return errors.Wrap(types.ErrWalletSignatureVerificationFailed, "invalid signature length")
|
|
}
|
|
|
|
// The recovery parameter needs to be adjusted for Ethereum
|
|
if signature[64] >= 27 {
|
|
signature[64] -= 27
|
|
}
|
|
|
|
publicKeyECDSA, err := crypto.SigToPub(messageHash.Bytes(), signature)
|
|
if err != nil {
|
|
return errors.Wrap(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"failed to recover public key",
|
|
)
|
|
}
|
|
|
|
// Get the address from the recovered public key
|
|
recoveredAddr := crypto.PubkeyToAddress(*publicKeyECDSA)
|
|
|
|
// Compare addresses
|
|
if recoveredAddr != expectedAddr {
|
|
return errors.Wrapf(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"signature verification failed: expected %s, got %s",
|
|
expectedAddr.Hex(),
|
|
recoveredAddr.Hex(),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifyCosmosSignature verifies a Cosmos signature using secp256k1
|
|
func (k Keeper) verifyCosmosSignature(
|
|
ctx context.Context,
|
|
walletAddress string,
|
|
challenge, signature []byte,
|
|
) error {
|
|
// Parse bech32 address to get account address
|
|
accAddr, err := sdk.AccAddressFromBech32(walletAddress)
|
|
if err != nil {
|
|
return errors.Wrapf(
|
|
types.ErrInvalidCosmosAddress,
|
|
"failed to parse bech32 address: %v",
|
|
err,
|
|
)
|
|
}
|
|
|
|
// Basic signature validation - Cosmos secp256k1 signatures are 64 bytes
|
|
if len(signature) != 64 {
|
|
return errors.Wrap(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"invalid signature length for Cosmos (expected 64 bytes)",
|
|
)
|
|
}
|
|
|
|
// Retrieve account from chain state using AccountKeeper
|
|
account := k.accountKeeper.GetAccount(ctx, accAddr)
|
|
if account == nil {
|
|
return errors.Wrapf(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"account not found for address: %s",
|
|
walletAddress,
|
|
)
|
|
}
|
|
|
|
// Extract public key from account
|
|
pubKey := account.GetPubKey()
|
|
if pubKey == nil {
|
|
return errors.Wrapf(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"no public key found for account: %s",
|
|
walletAddress,
|
|
)
|
|
}
|
|
|
|
// Ensure the public key is secp256k1
|
|
secp256k1PubKey, ok := pubKey.(*secp256k1.PubKey)
|
|
if !ok {
|
|
return errors.Wrapf(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"account public key is not secp256k1: %T",
|
|
pubKey,
|
|
)
|
|
}
|
|
|
|
// Verify signature against challenge using secp256k1
|
|
if !secp256k1PubKey.VerifySignature(challenge, signature) {
|
|
return errors.Wrapf(
|
|
types.ErrWalletSignatureVerificationFailed,
|
|
"signature verification failed for address: %s",
|
|
walletAddress,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CreateVerificationMethodFromWallet creates a W3C verification method for an external wallet
|
|
func (k Keeper) CreateVerificationMethodFromWallet(
|
|
methodID, controllerDID, walletAddress, chainID string,
|
|
walletType types.WalletType,
|
|
) (*types.VerificationMethod, error) {
|
|
// Validate wallet type
|
|
if err := walletType.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create blockchain account ID
|
|
accountID := types.BlockchainAccountID{
|
|
Namespace: walletType.GetNamespace(),
|
|
ChainID: chainID,
|
|
Address: walletAddress,
|
|
}
|
|
|
|
if err := accountID.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create verification method
|
|
verificationMethod := &types.VerificationMethod{
|
|
Id: methodID,
|
|
VerificationMethodKind: walletType.ToVerificationMethodType(),
|
|
Controller: controllerDID,
|
|
BlockchainAccountId: accountID.String(),
|
|
}
|
|
|
|
return verificationMethod, nil
|
|
}
|
|
|
|
// CheckWalletNotAlreadyLinked checks if a wallet is already linked to any DID
|
|
// by querying all DID documents and examining their verification methods for
|
|
// matching blockchain account IDs. Returns ErrWalletAlreadyLinked if found.
|
|
func (k Keeper) CheckWalletNotAlreadyLinked(
|
|
ctx any,
|
|
walletAddress, chainID string,
|
|
walletType types.WalletType,
|
|
) error {
|
|
// Convert context to SDK context for logging
|
|
sdkCtx, ok := ctx.(sdk.Context)
|
|
if !ok {
|
|
return errors.Wrap(types.ErrInvalidRequest, "invalid context type")
|
|
}
|
|
|
|
// Create the blockchain account ID we're looking for
|
|
accountID := types.BlockchainAccountID{
|
|
Namespace: walletType.GetNamespace(),
|
|
ChainID: chainID,
|
|
Address: walletAddress,
|
|
}
|
|
|
|
// Validate the account ID format before searching
|
|
if err := accountID.Validate(); err != nil {
|
|
return errors.Wrap(types.ErrInvalidBlockchainAccountID, err.Error())
|
|
}
|
|
|
|
targetAccountID := accountID.String()
|
|
|
|
k.logger.Debug("Checking wallet duplication",
|
|
"wallet_address", walletAddress,
|
|
"chain_id", chainID,
|
|
"wallet_type", walletType,
|
|
"target_account_id", targetAccountID,
|
|
)
|
|
|
|
// Use ORM iterator to efficiently scan all DID documents
|
|
iterator, err := k.OrmDB.DIDDocumentTable().List(sdkCtx, &apiv1.DIDDocumentPrimaryKey{})
|
|
if err != nil {
|
|
k.logger.Error("Failed to list DID documents for wallet duplication check", "error", err)
|
|
return errors.Wrap(types.ErrFailedToCheckDIDExists, err.Error())
|
|
}
|
|
defer iterator.Close()
|
|
|
|
// Iterate through all DID documents to check verification methods
|
|
for iterator.Next() {
|
|
ormDoc, err := iterator.Value()
|
|
if err != nil {
|
|
k.logger.Error(
|
|
"Failed to get DID document during wallet duplication check",
|
|
"error",
|
|
err,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Skip deactivated DID documents as their verification methods are no longer active
|
|
if ormDoc.Deactivated {
|
|
continue
|
|
}
|
|
|
|
// Convert from ORM type to access verification methods
|
|
didDoc := types.DIDDocumentFromORM(ormDoc)
|
|
|
|
// Check all verification methods for matching blockchain account ID
|
|
for _, vm := range didDoc.VerificationMethod {
|
|
// Skip verification methods without blockchain account IDs
|
|
if vm.BlockchainAccountId == "" {
|
|
continue
|
|
}
|
|
|
|
// Check for exact match with the wallet we're trying to link
|
|
if vm.BlockchainAccountId == targetAccountID {
|
|
k.logger.Info("Found duplicate wallet link",
|
|
"wallet_address", walletAddress,
|
|
"chain_id", chainID,
|
|
"wallet_type", walletType,
|
|
"existing_did", didDoc.Id,
|
|
"verification_method_id", vm.Id,
|
|
)
|
|
|
|
return errors.Wrapf(
|
|
types.ErrWalletAlreadyLinked,
|
|
"wallet %s on chain %s (%s) is already linked to DID %s in verification method %s",
|
|
walletAddress,
|
|
chainID,
|
|
walletType,
|
|
didDoc.Id,
|
|
vm.Id,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
k.logger.Debug("Wallet is not linked to any existing DID",
|
|
"wallet_address", walletAddress,
|
|
"chain_id", chainID,
|
|
"wallet_type", walletType,
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateDWNVaultController validates that the DID has an active DWN vault controller
|
|
func (k Keeper) ValidateDWNVaultController(ctx any, did string) error {
|
|
// This would check if the DID has an active DWN vault controller
|
|
// For now, we'll implement a basic check
|
|
|
|
// In a complete implementation, this would:
|
|
// 1. Query the DWN module to check if the DID has an active vault
|
|
// 2. Verify the vault is properly configured
|
|
// 3. Ensure the vault can sign transactions
|
|
|
|
// For now, we'll assume all DIDs are valid if they exist
|
|
return nil
|
|
}
|
|
|
|
// GenerateWalletChallenge generates a challenge message for wallet ownership proof
|
|
func (k Keeper) GenerateWalletChallenge(did, walletAddress string, blockHeight int64) []byte {
|
|
challengeMsg := fmt.Sprintf(
|
|
"Link wallet %s to DID %s at block %d. This proves ownership of the wallet.",
|
|
walletAddress, did, blockHeight,
|
|
)
|
|
return []byte(challengeMsg)
|
|
}
|
|
|
|
// Helper functions for signature verification
|
|
|
|
// recoverEthereumPublicKey recovers the public key from an Ethereum signature
|
|
func recoverEthereumPublicKey(message, signature []byte) (*ecdsa.PublicKey, error) {
|
|
if len(signature) != 65 {
|
|
return nil, fmt.Errorf("invalid signature length")
|
|
}
|
|
|
|
// Adjust recovery parameter
|
|
if signature[64] >= 27 {
|
|
signature[64] -= 27
|
|
}
|
|
|
|
hash := crypto.Keccak256Hash(message)
|
|
return crypto.SigToPub(hash.Bytes(), signature)
|
|
}
|
|
|
|
// verifySecp256k1Signature verifies a secp256k1 signature for Cosmos
|
|
func verifySecp256k1Signature(pubKey cryptotypes.PubKey, message, signature []byte) bool {
|
|
secp256k1PubKey, ok := pubKey.(*secp256k1.PubKey)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
return secp256k1PubKey.VerifySignature(message, signature)
|
|
}
|