You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用AWS KMS签名以太坊时R/S值异常,无法生成可恢复密钥

解决AWS KMS签名以太坊时Could not construct a recoverable key异常问题

问题场景

使用AWS KMS服务对以太坊数据签名,执行Sign.createSignatureData(ecdsaSignature,pubKeyToBigInter(publicKey),Hash.sha3(Bytes))时抛出异常:

Could not construct a recoverable key. Are your credentials valid?

初步判断是parseDERSequence方法返回的R、S值不正确导致失败,相关代码见下文。

核心问题分析

  1. 摘要算法不匹配:Web3j的Sign.createSignatureData在恢复公钥时,依赖原始数据的Keccak-256哈希(即Hash.sha3(Bytes)),但当前代码传给KMS签名的是原始数据的SHA-256哈希,两者摘要完全不同,导致公钥恢复验证失败。
  2. S值不符合以太坊规范:AWS KMS生成的ECDSA签名中,S值可能大于secp256k1曲线阶的一半,而以太坊强制要求S为低值(≤曲线阶的一半),否则无法正确恢复公钥。
  3. 密钥曲线可能不匹配:如果AWS KMS创建的密钥不是secp256k1曲线(以太坊专用曲线),而是默认的secp256r1,也会导致公钥恢复失败。

解决方案

1. 统一摘要算法

将传给KMS的签名消息改为原始数据的Keccak-256哈希,并设置MessageType=RAW,避免KMS再次对消息做SHA-256哈希。

2. 处理S值为低S规范

解析KMS返回的签名后,检查S值是否超过secp256k1曲线阶的一半,若超过则用曲线阶减去S值得到合规的低S值。

3. 确认KMS密钥曲线

确保AWS KMS中创建的密钥是secp256k1曲线类型,而非默认的secp256r1。

修改后的完整代码

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.AWSKMSClientBuilder;
import com.amazonaws.services.kms.model.GetPublicKeyRequest;
import com.amazonaws.services.kms.model.SignRequest;
import com.amazonaws.util.BinaryUtils;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.junit.Test;
import org.web3j.crypto.ECDSASignature;
import org.web3j.crypto.Hash;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

@Slf4j
public class AwsKmsTest {

    private static final String keyId = "****";
    // secp256k1曲线阶和半阶常量
    private static final BigInteger SECP256K1_CURVE_ORDER = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16);
    private static final BigInteger SECP256K1_HALF_CURVE_ORDER = new BigInteger("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0", 16);
    
    AWSKMS kmsClient = AWSKMSClientBuilder.standard()
            .withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials("****", "*****")))
            .withRegion(Regions.AP_NORTHEAST_1)
            .build();
    
    
    static {
        if (Security.getProvider("BC") == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }
    
    @Test
    public void publicKey() throws Exception {
        BCECPublicKey publicKey = (BCECPublicKey) getPublicKey();
        System.out.println(Keys.getAddress(pubKeyToBigInter(publicKey)));
    }
    
    @Test
    public void sign() throws Exception {
        String msg = "hi";
        byte[] Bytes = msg.getBytes(StandardCharsets.UTF_8);
        // 使用Web3j的Keccak-256哈希,和恢复逻辑保持一致
        byte[] digestBytes = Hash.sha3(Bytes);
    
        SignRequest signRequest = new SignRequest();
        signRequest.setKeyId(keyId);
        signRequest.setMessage(ByteBuffer.wrap(digestBytes));
        signRequest.setMessageType("RAW"); // 告知KMS这是已哈希的原始数据
        signRequest.setSigningAlgorithm("ECDSA_SHA_256");
        ByteBuffer signatureB = kmsClient.sign(signRequest).getSignature();
    
        BCECPublicKey publicKey = (BCECPublicKey) getPublicKey();
    
        // 验证签名(可选,用于确认KMS签名正确性)
        Signature signature = Signature.getInstance("SHA256withECDSA", "BC");
        signature.initVerify(publicKey);
        signature.update(digestBytes); // 注意这里更新的是Keccak哈希后的数据,因为KMS签名的是这个值
        if (signature.verify(signatureB.array())) {
            ECDSASignature ecdsaSignature = parseDERSequence(signatureB.array());
            // 生成以太坊兼容的签名数据
            Sign.SignatureData signMessage = Sign.createSignatureData(ecdsaSignature,
                    pubKeyToBigInter(publicKey),
                    Hash.sha3(Bytes));
            System.out.println("0x" + Hex.toHexString(signMessage.getR()) + Hex.toHexString(signMessage.getS()) + Hex.toHexString(signMessage.getV()));
        }
    
    }
    
    public static BigInteger pubKeyToBigInter(BCECPublicKey publicKey) {
        byte[] publicKeyBytes = publicKey.getQ().getEncoded(false);
        return new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length));
    }
    
    public PublicKey getPublicKey() throws Exception {
        GetPublicKeyRequest publicKeyRequest = new GetPublicKeyRequest();
        publicKeyRequest.setKeyId(keyId);
        ByteBuffer publicKeyByteBuffer = kmsClient.getPublicKey(publicKeyRequest).getPublicKey();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(BinaryUtils.copyBytesFrom(publicKeyByteBuffer));
        KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
        return keyFactory.generatePublic(keySpec);
    }
    
    
    private ECDSASignature parseDERSequence(byte[] derEncoded) throws IOException {
        ASN1InputStream asn1InputStream = new ASN1InputStream(derEncoded);
        ASN1Sequence asn1Sequence = (ASN1Sequence) asn1InputStream.readObject();
        ASN1Integer r = (ASN1Integer) asn1Sequence.getObjectAt(0);
        ASN1Integer s = (ASN1Integer) asn1Sequence.getObjectAt(1);
        BigInteger rValue = r.getPositiveValue();
        BigInteger sValue = s.getPositiveValue();
        
        // 将S值转换为以太坊要求的低S值
        if (sValue.compareTo(SECP256K1_HALF_CURVE_ORDER) > 0) {
            sValue = SECP256K1_CURVE_ORDER.subtract(sValue);
        }
        
        return new ECDSASignature(rValue, sValue);
    }

}

关键修改点说明

  • 替换摘要生成逻辑:用Hash.sha3(Bytes)替代SHA-256哈希,确保和Web3j恢复逻辑一致。
  • 修改KMS签名请求:设置MessageType=RAW,告知KMS直接对传入的Keccak哈希值签名。
  • 添加低S值处理:在parseDERSequence中对S值做合规性转换。
  • 调整签名验证逻辑:验证时更新的是Keccak哈希后的数据,匹配KMS的签名对象。

内容的提问来源于stack exchange,提问作者carlos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 16:29:50