hashgraph / hedera-sdk-go

Hedera™ Hashgraph SDK for Go
https://docs.hedera.com/docs/hedera-sdks
Apache License 2.0
89 stars 64 forks source link

INVALID_SIGNATURE when attempting to submit a tx signed with an ECDSA(secp256k1) key #1060

Closed jbaldwinroberts closed 1 week ago

jbaldwinroberts commented 1 week ago

Description

I'm attempting to sign and submit a hedera transaction using a ECC_SECG_P256K1 key from AWS KMS, here is an example public key: 04fcf808c1dd96931eb68a26f20f303952a263786ebfa049d219b897e96409f090dc884790406db423f6b7c5d23e69dfbec438d8e5df45a03fd140c01feef46287

I have been able to successfully sign a transaction following the guidance here, below is a code snippet showing how I generate the digest:

func getFrozenTransactionBodyBytesHash(transaction interfaces.Transaction) ([]byte, *rTypes.Error) {
    signedTransaction := services.SignedTransaction{}
    if err := prototext.Unmarshal([]byte(transaction.String()), &signedTransaction); err != nil {
        return nil, errors.ErrTransactionUnmarshallingFailed
    }

    // https://hips.hedera.com/hip/hip-222
    hash := sha3.NewLegacyKeccak256()
    hash.Write([]byte(hex.EncodeToString(signedTransaction.BodyBytes)))
    hashSum := hash.Sum(nil)

    return hashSum, nil
}

Signing this results in a signature, example: 3045022100f81bef704cf384febb50671ece9c6b0e27ddb05213b4eeaa9a5d6d23a6c96ed7022026a8b7317f8a745dc9090e574c3938171872099d0b81179352e7853f6b532e77

I'm able to successfully verify both the ASN.1 encoded sig, and the raw 64-byte sig, below is a code snippet:

        // This passes
        if !ecdsa.VerifyASN1(pk, digest, signature.Bytes) {
            log.Error("original signature verification failed")
        }

        // This fails
        if !ecdsa.VerifyASN1(pk, digest, rawSignature) {
            log.Error("raw signature verification failed")
        }

        // This passes
        if !hpk.Verify(digest, rawSignature) {
            log.Error("hedera public key verification failed")
        }

I'm able to add the signature using func (tx *TransferTransaction) AddSignature(publicKey PublicKey, signature []byte) *TransferTransaction

However, when I execute the transaction I receive the following error:

time="2024-09-10T16:59:23+01:00" level=error msg="Failed to execute transaction 0.0.4843569@1725983859.511142833: exceptional precheck status INVALID_SIGNATURE received for transaction 0.0.4843569@1725983859.511142833"

Any ideas what I could be doing wrong, or how to debug this further? AFAICT I am meeting the criteria specified.

I realise this is not a minimal reproducible example, i'll work on that next, working backwards from a working example.

Steps to reproduce

Execute the code below:

Test case:

func Test_ConstructionCombine(t *testing.T) {
    ctx := context.Background()

    hc := hedera.ClientForTestnet()
    c := New(0, 0, hc, construction.NewTransactionConstructor())

    pk, err := hex.DecodeString("04fcf808c1dd96931eb68a26f20f303952a263786ebfa049d219b897e96409f090dc884790406db423f6b7c5d23e69dfbec438d8e5df45a03fd140c01feef46287")
    require.NoError(t, err)

    sig, err := hex.DecodeString("3045022100f81bef704cf384febb50671ece9c6b0e27ddb05213b4eeaa9a5d6d23a6c96ed7022026a8b7317f8a745dc9090e574c3938171872099d0b81179352e7853f6b532e77")
    require.NoError(t, err)

    resp, rerr := c.ConstructionCombine(
        ctx, &rTypes.ConstructionCombineRequest{
            UnsignedTransaction: "0x0a442a420a3e0a140a0b08d6c281b70610c688c24e120518b1d0a702120218071880c2d72f220308b40172180a160a090a0518b1d0a70210130a090a0518b3d0a70210141200",
            Signatures: []*rTypes.Signature{
                {
                    PublicKey: &rTypes.PublicKey{
                        Bytes:     pk,
                        CurveType: rTypes.Secp256k1,
                    },
                    SignatureType: rTypes.Ecdsa,
                    Bytes:         sig,
                },
            },
        },
    )
    log.Infof("ConstructionCombine: %+v", resp)
    log.Infof("ConstructionCombine: %+v", rerr)
}

Code under test:

func (h *Handler) ConstructionCombine(
    _ context.Context,
    request *rTypes.ConstructionCombineRequest,
) (*rTypes.ConstructionCombineResponse, *rTypes.Error) {
    if len(request.Signatures) == 0 {
        return nil, errors.ErrNoSignature
    }

    transaction, rErr := unmarshallTransactionFromHexString(request.UnsignedTransaction)
    if rErr != nil {
        return nil, rErr
    }

    frozenBodyBytes, rErr := getFrozenTransactionBodyBytesHash(transaction)
    if rErr != nil {
        return nil, rErr
    }

    log.WithField("unsigned transaction", request.UnsignedTransaction).Info("Unsigned transaction")

    for _, signature := range request.Signatures {
        if signature.SignatureType != rTypes.Ecdsa {
            return nil, errors.ErrInvalidSignatureType
        }

        pk, err := crypto.UnmarshalPubkey(signature.PublicKey.Bytes)
        if err != nil {
            log.WithError(err).Error("Failed to unmarshal public key")
            return nil, errors.ErrInvalidPublicKey
        }

        hpk, err := hedera.PublicKeyFromBytesECDSA(crypto.CompressPubkey(pk))
        if err != nil {
            log.WithError(err).Error("Failed to create public key from compressed public key")
            return nil, errors.ErrInvalidPublicKey
        }

        pk, err = crypto.DecompressPubkey(hpk.BytesRaw())
        if err != nil {
            log.WithError(err).Error("Failed to decompress public key")
            return nil, errors.ErrInvalidPublicKey
        }

        rawSignature, err := convertASN1ToRaw(signature.Bytes)
        if err != nil {
            log.WithError(err).Error("Failed to convert ASN1 to raw signature")
            return nil, errors.ErrInvalidSignatureVerification
        }

        log.WithFields(
            log.Fields{
                "pk":        hex.EncodeToString(signature.PublicKey.Bytes),
                "hash":      hex.EncodeToString(frozenBodyBytes),
                "signature": hex.EncodeToString(signature.Bytes),
            },
        ).Info("about to verify signature")

        // This passes
        if !ecdsa.VerifyASN1(pk, frozenBodyBytes, signature.Bytes) {
            log.Error("original signature verification failed")
        }

        // This fails
        if !ecdsa.VerifyASN1(pk, frozenBodyBytes, rawSignature) {
            log.Error("raw signature verification failed")
        }

        // This passes
        if !hpk.Verify(frozenBodyBytes, rawSignature) {
            log.Error("hedera public key verification failed")
        }

        // This passes
        if rErr = addSignature(transaction, hpk, rawSignature); rErr != nil {
            return nil, rErr
        }
    }

    // This returns
    // time="2024-09-10T16:59:23+01:00" level=error msg="Failed to execute transaction 0.0.4843569@1725983859.511142833: exceptional precheck status INVALID_SIGNATURE received for transaction 0.0.4843569@1725983859.511142833"
    // _, err := transaction.Execute(h.hederaClient)
    // if err != nil {
    //  log.Errorf(
    //      "Failed to execute transaction %s: %s", transaction.GetTransactionID(), err,
    //  )
    //  return nil, errors.AddErrorDetails(
    //      errors.ErrTransactionSubmissionFailed,
    //      "reason",
    //      fmt.Sprintf("%s", err),
    //  )
    // }

    transactionBytes, err := transaction.ToBytes()
    if err != nil {
        return nil, errors.ErrTransactionMarshallingFailed
    }

    return &rTypes.ConstructionCombineResponse{
        SignedTransaction: tools.SafeAddHexPrefix(hex.EncodeToString(transactionBytes)),
    }, nil
}

func unmarshallTransactionFromHexString(transactionString string) (interfaces.Transaction, *rTypes.Error) {
    transactionBytes, err := hex.DecodeString(tools.SafeRemoveHexPrefix(transactionString))
    if err != nil {
        return nil, errors.ErrTransactionDecodeFailed
    }

    transaction, err := hedera.TransactionFromBytes(transactionBytes)
    if err != nil {
        return nil, errors.ErrTransactionUnmarshallingFailed
    }

    switch tx := transaction.(type) {
    // these transaction types are what the construction service supports
    case hedera.AccountCreateTransaction:
        return &tx, nil
    case hedera.TokenAssociateTransaction:
        return &tx, nil
    case hedera.TokenBurnTransaction:
        return &tx, nil
    case hedera.TokenCreateTransaction:
        return &tx, nil
    case hedera.TokenDeleteTransaction:
        return &tx, nil
    case hedera.TokenDissociateTransaction:
        return &tx, nil
    case hedera.TokenFreezeTransaction:
        return &tx, nil
    case hedera.TokenGrantKycTransaction:
        return &tx, nil
    case hedera.TokenMintTransaction:
        return &tx, nil
    case hedera.TokenRevokeKycTransaction:
        return &tx, nil
    case hedera.TokenUnfreezeTransaction:
        return &tx, nil
    case hedera.TokenUpdateTransaction:
        return &tx, nil
    case hedera.TokenWipeTransaction:
        return &tx, nil
    case hedera.TransferTransaction:
        return &tx, nil
    default:
        return nil, errors.ErrTransactionInvalidType
    }
}

func getFrozenTransactionBodyBytesHash(transaction interfaces.Transaction) ([]byte, *rTypes.Error) {
    signedTransaction := services.SignedTransaction{}
    if err := prototext.Unmarshal([]byte(transaction.String()), &signedTransaction); err != nil {
        return nil, errors.ErrTransactionUnmarshallingFailed
    }

    // https://hips.hedera.com/hip/hip-222
    hash := sha3.NewLegacyKeccak256()
    hash.Write([]byte(hex.EncodeToString(signedTransaction.BodyBytes)))
    hashSum := hash.Sum(nil)

    return hashSum, nil
}

type ecdsaSignature struct {
    R, S *big.Int
}

func convertASN1ToRaw(signature []byte) ([]byte, error) {
    var esig ecdsaSignature
    _, err := asn1.Unmarshal(signature, &esig)
    if err != nil {
        return nil, fmt.Errorf("error unmarshalling signature: %v", err)
    }

    // Ensure r and s are 32 bytes each
    rBytes := esig.R.Bytes()
    sBytes := esig.S.Bytes()

    // Pad r and s to 32 bytes if necessary
    rPadded := make([]byte, 32)
    sPadded := make([]byte, 32)
    copy(rPadded[32-len(rBytes):], rBytes)
    copy(sPadded[32-len(sBytes):], sBytes)

    // Concatenate r and s
    rawSignature := append(rPadded, sPadded...)

    return rawSignature, nil
}

func addSignature(transaction interfaces.Transaction, pubKey hedera.PublicKey, signature []byte) *rTypes.Error {
    switch tx := transaction.(type) {
    // these transaction types are what the construction service supports
    case *hedera.AccountCreateTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenAssociateTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenBurnTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenCreateTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenDeleteTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenDissociateTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenFreezeTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenGrantKycTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenMintTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenRevokeKycTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenUnfreezeTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenUpdateTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TokenWipeTransaction:
        tx.AddSignature(pubKey, signature)
    case *hedera.TransferTransaction:
        tx.AddSignature(pubKey, signature)
    default:
        return errors.ErrTransactionInvalidType
    }

    return nil
}

Additional context

No response

Hedera network

testnet

Version

v2.44.0

Operating system

macOS