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

Java Card生成的ECDSA签名无法被C# Bouncy Castle验证求助

解决Java Card ECDSA签名在C# BouncyCastle验证失败的问题

看起来你遇到的问题我之前也碰到过——其他验证路径都正常,唯独Java Card生成的签名在C# BouncyCastle里通不过,核心原因其实是两者默认的ECDSA签名格式不一样,咱们一步步来解决:

为啥会验证失败?

Java Card用ALG_ECDSA_SHA_256生成的签名是原始的(r, s)拼接格式:P-256曲线下,r和s都是256位,所以直接把32字节的r和32字节的s按大端顺序拼起来,得到64字节的签名。

而C# BouncyCastle的"SHA-256withECDSA"签名器默认认的是ASN.1 DER编码格式的签名——这种格式是用DER序列把r和s包裹起来,长度通常在68-72字节左右,和原始拼接的64字节完全不是一回事,自然验证失败。

两种解决方案任你选

方案1:改C#代码,直接解析原始(r,s)签名

不用改卡端代码,直接在C#里把64字节的签名拆成r和s,用ECDSASigner直接验证,跳过默认的DER解析:

public bool HashAndVerifyDSA(byte[] pb, byte[] inData, byte[] sig)
{
    // 先确认签名是64字节的原始格式
    if (sig.Length != 64)
        throw new ArgumentException("Invalid raw ECDSA signature length");
    
    // 这段公钥构造和你原来的代码一致,不用改
    byte[] pub = new byte[65];
    pub[0] = 0x4;
    Array.Copy(pb, 0, pub, 1, 64);
    ECCurve curve = parameters.Curve;
    ECPoint q = curve.DecodePoint(pub);
    ICipherParameters publicKey = new ECPublicKeyParameters(algorithm, q, parameters);
    
    // 把64字节签名拆成r和s各32字节
    byte[] rBytes = new byte[32];
    byte[] sBytes = new byte[32];
    Array.Copy(sig, 0, rBytes, 0, 32);
    Array.Copy(sig, 32, sBytes, 0, 32);
    
    // 转成BigInteger(注意用大端模式,和Java Card一致)
    BigInteger r = new BigInteger(1, rBytes);
    BigInteger s = new BigInteger(1, sBytes);
    
    // 用ECDSASigner直接验证原始签名
    ECDSASigner signer = new ECDSASigner(new HMacDsaKCalculator(new Sha256Digest()));
    signer.Init(false, publicKey);
    // 先计算数据的SHA-256哈希,因为Java Card的ALG_ECDSA_SHA_256已经包含哈希步骤
    return signer.VerifySignature(ComputeSHA256(inData), r, s);
}

// 辅助方法:计算SHA-256哈希
private byte[] ComputeSHA256(byte[] data)
{
    Sha256Digest digest = new Sha256Digest();
    digest.BlockUpdate(data, 0, data.Length);
    byte[] hash = new byte[digest.GetDigestSize()];
    digest.DoFinal(hash, 0);
    return hash;
}

方案2:改Java Card代码,返回DER格式签名

如果你更愿意在卡端处理格式,可以把原始的(r,s)签名编码成ASN.1 DER格式,这样C#端原来的代码就能直接用了。DER编码的ECDSA签名是一个SEQUENCE,包含r和s两个整数,卡上可以手动实现简单的编码逻辑(尽量轻量,适合Java Card的资源限制):

// 假设sig是卡内生成的64字节原始签名(r在前32字节,s在后32字节)
byte[] r = new byte[32];
byte[] s = new byte[32];
System.arraycopy(sig, 0, r, 0, 32);
System.arraycopy(sig, 32, s, 0, 32);

// 把r和s编码成DER整数格式
byte[] derR = encodeInteger(r);
byte[] derS = encodeInteger(s);

// 构造DER SEQUENCE:0x30 + 总长度 + derR + derS
int totalLength = derR.length + derS.length;
byte[] derSignature = new byte[2 + totalLength];
derSignature[0] = 0x30; // SEQUENCE标签
derSignature[1] = (byte)totalLength;
System.arraycopy(derR, 0, derSignature, 2, derR.length);
System.arraycopy(derS, 0, derSignature, 2 + derR.length, derS.length);

// 返回derSignature给C#端即可

对应的encodeInteger方法(处理整数的DER编码,去掉前导零,处理负数标记):

private byte[] encodeInteger(byte[] rawBytes) {
    int startIndex = 0;
    // 跳过前导零
    while (startIndex < rawBytes.length && rawBytes[startIndex] == 0) {
        startIndex++;
    }
    // 全零的特殊情况
    if (startIndex == rawBytes.length) {
        return new byte[]{0x02, 0x01, 0x00};
    }
    // 如果最高位是1,需要加一个零字节避免被解析为负数
    boolean needZeroPrefix = (rawBytes[startIndex] & 0x80) != 0;
    int encodedLength = rawBytes.length - startIndex + (needZeroPrefix ? 1 : 0);
    byte[] result = new byte[2 + encodedLength];
    result[0] = 0x02; // INTEGER类型标签
    result[1] = (byte)encodedLength;
    int destPos = 2;
    if (needZeroPrefix) {
        result[destPos++] = 0x00;
    }
    System.arraycopy(rawBytes, startIndex, result, destPos, rawBytes.length - startIndex);
    return result;
}

最后再检查两个点

  • 确认Java Card生成的签名中r和s是大端字节序(Java Card的Signature.sign默认就是大端,和BouncyCastle的BigInteger解析逻辑一致)。
  • 确保C#端的parameters确实是P-256曲线(也就是NIST P-256/secp256r1),别不小心用了其他曲线参数。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 06:33:00