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

基于远程签名服务的签名创建CMS签名遇验证错误,如何修复?

CMS签名验证失败问题排查与修复

问题描述

我通过Remote Signing Service生成签名后创建CMS签名,操作流程如下:

  1. 对需签名的消息进行SHA256哈希
  2. 将哈希值发送至Remote Signing Service获取签名
  3. 使用以下Java代码生成包含该签名的p7m文件
public static void signMessage() {
    String messageToBeSign = "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.";
    byte[] dataToSign = messageToBeSign.getBytes();
    
    // Log hashed data to send it to Remote Signing Service
    // MessageDigest digest = MessageDigest.getInstance("SHA-256");
    // byte[] hashedData = digest.digest(dataToSign);
    // System.out.println(Base64.getEncoder().encodeToString(hashedData));

    // Load signature recieved from Remote Signing Service
    byte[] remoteSignature = Base64.getDecoder().decode(remoteSignatureBase64String);
    Security.addProvider(new BouncyCastleProvider());

    // Load signing certificate
    byte[] certBytes = Base64.getDecoder().decode(base64CertChain[0]);
    X509CertificateHolder certHolder = new X509CertificateHolder(certBytes);

    DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();
    JcaSignerInfoGeneratorBuilder signerInfoGenBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
    ContentSigner contentSigner = new PrecomputedSigner(remoteSignature);
    SignerInfoGenerator signerInfoGenerator = signerInfoGenBuilder.build(contentSigner, certHolder);

    // Load full certificate chain
    List<X509CertificateHolder> certChain = new ArrayList<>();
    for (String base64Cert : base64CertChain) {
        certChain.add(new X509CertificateHolder(Base64.getDecoder().decode(base64Cert)));
    }
    Store certStore = new JcaCertStore(certChain);
    
    CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
    generator.addSignerInfoGenerator(signerInfoGenerator);
    generator.addCertificates(certStore);

    CMSProcessableByteArray cmsData = new CMSProcessableByteArray(dataToSign);
    CMSSignedData signedData = generator.generate(cmsData, true);
    byte[] p7m = signedData.getEncoded();
    
    try (FileOutputStream fos = new FileOutputStream(signedFileOutput)) {
        fos.write(p7m);
    }
    
    System.out.println("Successfully create a new signature to the .p7m file!");
}

//Helper class to handle the precomputed signature from the Remote Signing Service
class PrecomputedSigner implements ContentSigner {
    private final byte[] signature;

    public PrecomputedSigner(byte[] signature) {
        this.signature = signature;
    }

    @Override
    public byte[] getSignature() {
        return signature;
    }

    @Override
    public AlgorithmIdentifier getAlgorithmIdentifier() {
       return new AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption);
    }

    @Override
    public OutputStream getOutputStream() {
        return new ByteArrayOutputStream(); // Not used since the signature is precomputed
    }
}

生成p7m文件后,使用DSS Demonstration WebApp验证时出现以下错误:

  • The signature is not intact!
  • The signed attribute: 'signing-certificate' is absent!

错误原因

  1. 签名不完整:你错误地对原始消息的哈希进行了远程签名,但CMS签名的实际签名对象是签名属性集合的哈希值。验证时DSS会重新计算签名属性集合的哈希,再和签名解密后的哈希对比,两者不匹配导致验证失败。
  2. 缺少signing-certificate属性:默认配置下JcaSignerInfoGeneratorBuilder不会自动添加这个属性,而DSS验证要求该属性必须存在,用于明确标识签名证书。

修复方案

1. 调整远程签名的对象

需要先构造CMS的签名属性集合,对该集合的哈希值进行远程签名,而不是原始消息的哈希。

2. 添加signing-certificate签名属性

通过JcaSignerInfoGeneratorBuilder配置添加该属性,同时修正PrecomputedSigner的逻辑,确保签名对应正确的哈希对象。

修改后的完整代码

import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cms.*;
import org.bouncycastle.cms.jcajce.JcaCertStore;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.x509.X509CertificateHolder;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.Security;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;

public class CMSSignatureFix {
    private static String remoteSignatureBase64String; // 从Remote Signing Service获取的签名
    private static String[] base64CertChain; // 完整证书链的Base64数组
    private static String signedFileOutput; // 输出p7m文件路径

    public static void signMessage() throws IOException, OperatorCreationException {
        String messageToBeSign = "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.";
        byte[] dataToSign = messageToBeSign.getBytes();

        Security.addProvider(new BouncyCastleProvider());

        // 加载签名证书
        byte[] certBytes = Base64.getDecoder().decode(base64CertChain[0]);
        X509CertificateHolder certHolder = new X509CertificateHolder(certBytes);

        // 1. 构建签名属性集合(包含signing-certificate属性)
        DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();
        JcaSignerInfoGeneratorBuilder signerInfoGenBuilder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
        
        // 添加signing-certificate签名属性
        signerInfoGenBuilder.setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator() {
            @Override
            public org.bouncycastle.asn1.cms.AttributeTable getAttributes(org.bouncycastle.asn1.cms.AttributeTable parameters) {
                org.bouncycastle.asn1.cms.Attribute signingCertAttr = new org.bouncycastle.asn1.cms.Attribute(
                        PKCSObjectIdentifiers.id_aa_signingCertificate,
                        new org.bouncycastle.asn1.DERSet(new org.bouncycastle.asn1.x509.SigningCertificate(
                                new org.bouncycastle.asn1.x509.IssuerSerial(
                                        certHolder.getIssuer(),
                                        certHolder.getSerialNumber()
                                )
                        ))
                );
                return super.getAttributes(parameters).add(signingCertAttr);
            }
        });

        // 2. 预计算签名属性的哈希(实际使用时先执行这段获取哈希,发送到远程服务拿签名)
        /*
        ContentSigner dummySigner = new ContentSigner() {
            @Override
            public org.bouncycastle.asn1.x509.AlgorithmIdentifier getAlgorithmIdentifier() {
                return new org.bouncycastle.asn1.x509.AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption);
            }

            @Override
            public OutputStream getOutputStream() {
                return new ByteArrayOutputStream();
            }

            @Override
            public byte[] getSignature() {
                return new byte[0];
            }
        };
        SignerInfoGenerator tempSignerInfoGen = signerInfoGenBuilder.build(dummySigner, certHolder);
        CMSSignedDataGenerator tempGenerator = new CMSSignedDataGenerator();
        tempGenerator.addSignerInfoGenerator(tempSignerInfoGen);
        CMSProcessableByteArray tempCmsData = new CMSProcessableByteArray(dataToSign);
        CMSSignedData tempSignedData = tempGenerator.generate(tempCmsData, false);
        SignerInformation signerInfo = tempSignedData.getSignerInfos().getSigners().iterator().next();
        byte[] signedAttrBytes = signerInfo.getSignedAttributes().getEncoded(ASN1Encoding.DER);
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashToSend = digest.digest(signedAttrBytes);
        System.out.println(Base64.getEncoder().encodeToString(hashToSend)); // 发送此哈希到远程服务
        */

        // 加载远程签名服务返回的签名
        byte[] remoteSignature = Base64.getDecoder().decode(remoteSignatureBase64String);
        ContentSigner contentSigner = new PrecomputedSigner(remoteSignature);

        // 构建签名器信息生成器
        SignerInfoGenerator signerInfoGenerator = signerInfoGenBuilder.build(contentSigner, certHolder);

        // 加载完整证书链
        List<X509CertificateHolder> certChain = new ArrayList<>();
        for (String base64Cert : base64CertChain) {
            certChain.add(new X509CertificateHolder(Base64.getDecoder().decode(base64Cert)));
        }
        JcaCertStore certStore = new JcaCertStore(certChain);

        // 生成CMS签名数据
        CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
        generator.addSignerInfoGenerator(signerInfoGenerator);
        generator.addCertificates(certStore);

        CMSProcessableByteArray cmsData = new CMSProcessableByteArray(dataToSign);
        CMSSignedData signedData = generator.generate(cmsData, true);
        byte[] p7m = signedData.getEncoded();

        // 写入文件
        try (FileOutputStream fos = new FileOutputStream(signedFileOutput)) {
            fos.write(p7m);
        }

        System.out.println("Successfully created .p7m file!");
    }

    // 修正后的PrecomputedSigner
    static class PrecomputedSigner implements ContentSigner {
        private final byte[] signature;

        public PrecomputedSigner(byte[] signature) {
            this.signature = signature;
        }

        @Override
        public org.bouncycastle.asn1.x509.AlgorithmIdentifier getAlgorithmIdentifier() {
            return new org.bouncycastle.asn1.x509.AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption);
        }

        @Override
        public OutputStream getOutputStream() {
            return new ByteArrayOutputStream(); // 未使用,因为签名是预计算的
        }

        @Override
        public byte[] getSignature() {
            return signature;
        }
    }
}

关键修改说明

  • 添加signing-certificate属性:通过重写DefaultSignedAttributeTableGenerator的getAttributes方法,手动添加该签名属性,满足DSS验证的合规要求。
  • 调整签名哈希对象:现在需要发送签名属性集合的哈希给远程签名服务,而不是原始消息的哈希。代码中注释部分展示了如何获取这个哈希值,实际使用时先执行该部分获取哈希,发送到远程服务得到签名后,再继续执行后续代码。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 19:59:51