使用Bouncy Castle 1.70验证无签名属性签名的可行性咨询
问题背景与代码场景
我在一个使用Bouncy Castle进行数字签名验证的项目中遇到了问题。原有验签方法如下:
private Set<TLName> verifySignature(PDSignature sig, X509Certificate signerCertificate, SignerInformation signerInfo, List<X509Certificate> chain, SignatureInfo signatureInfo) throws OperatorCreationException { Set<TLName> emptyTrustedBy = new HashSet<>(TLName.values().length); // checking cert validity at signing time try { signerCertificate.checkValidity(sig.getSignDate().getTime()); } catch (CertificateExpiredException e) { LOG.info("Signature verification failed, certificate is expired."); return emptyTrustedBy; } catch (CertificateNotYetValidException e) { LOG.info("Signature verification failed, certificate is not yet valid."); return emptyTrustedBy; } // check all certificates from chain on trust lists Set<TLName> filledTrustedBy = TrustedCertificates.getTrustedByForChain(chain.toArray(new X509Certificate[0])); if (filledTrustedBy.isEmpty()) { LOG.info("Signature verification failed, certificate not found between our trusted certificates."); return emptyTrustedBy; } // verifying signature with public key SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().build(signerCertificate); try { if (signerInfo.verify(verifier)) { signatureInfo.signedBytesUnchanged = true; signatureInfo.checksum = DatatypeConverter.printHexBinary(signerInfo.getContentDigest()); LOG.debug("Signature verification completed, signature verified."); return filledTrustedBy; // only here return trustedBy array filled with values } signatureInfo.checksum = DatatypeConverter.printHexBinary(signerInfo.getContentDigest()); } catch (CMSException e) { LOG.info("Signature verification failed, couldn't verify signature. Exception message: " + e.getMessage()); } return emptyTrustedBy; }
之前有额外校验层自行计算校验和供其他组件检查,代码如下:
private void checkSignedBytes(SignerInformation signerInfo, File inFile, int[] byteRange, SignatureInfo signatureInfo) throws NoSuchAlgorithmException, IOException { Attribute attribute = signerInfo.getSignedAttributes().get(CMSAttributes.messageDigest); DEROctetString sigDigest = (DEROctetString) attribute.getAttrValues().getObjectAt(0); byte[] sigDigestBytes = sigDigest.getOctets(); byte[] contentDigestBytes = Signer.getDigest(inFile, byteRange, signerInfo.getDigestAlgOID()); signatureInfo.checksum = DatatypeConverter.printHexBinary(sigDigestBytes); signatureInfo.signedBytesUnchanged = Arrays.equals(sigDigestBytes, contentDigestBytes); }
这个额外校验方法在遇到无签名属性的数字签名时失效。我研究了Bouncy Castle的SignerInformation类中doVerify(...)方法,发现它会处理无签名属性的签名,相关摘要计算和验证逻辑如下:
摘要计算逻辑
if (signedAttributeSet == null) { if (contentVerifier instanceof RawContentVerifier) { content.write(digOut); } else { OutputStream cOut = new TeeOutputStream(digOut, sigOut); content.write(cOut); cOut.close(); } }
验证逻辑
if (signedAttributeSet == null && resultDigest != null) { if (contentVerifier instanceof RawContentVerifier) { RawContentVerifier rawVerifier = (RawContentVerifier)contentVerifier; if (encName.equals("RSA")) { DigestInfo digInfo = new DigestInfo(new AlgorithmIdentifier(digestAlgorithm.getAlgorithm(), DERNull.INSTANCE), resultDigest); return rawVerifier.verify(digInfo.getEncoded(ASN1Encoding.DER), this.getSignature()); } return rawVerifier.verify(resultDigest, this.getSignature()); } }
我的疑问
我已经将checkSignedBytes(...)方法的逻辑移到verifySignature(...)方法中,具体修改如下:
if (signerInfo.verify(verifier)) { signatureInfo.signedBytesUnchanged = true; signatureInfo.checksum = DatatypeConverter.printHexBinary(signerInfo.getContentDigest()); LOG.debug("Signature verification completed, signature verified."); return filledTrustedBy; // only here return trustedBy array filled with values }
请问这样处理是否可行?是否会因缺少签名属性而丢失信息?由于其他组件需要这些属性,我必须初始化它们,即使看起来冗余。
回答
可行性结论
这种迁移是完全可行的,且能解决原方法在无签名属性场景下失效的问题。
核心分析
getContentDigest()的兼容性
Bouncy Castle的SignerInformation.getContentDigest()内部已兼容两种场景:- 存在签名属性时,从
CMSAttributes.messageDigest属性提取摘要值; - 无签名属性时,返回
doVerify过程中计算得到的resultDigest(即你贴出的验证逻辑中的值)。
无论签名是否带属性,该方法都能正确获取对应摘要,不会丢失信息。
- 存在签名属性时,从
signedBytesUnchanged的正确性
原方法通过对比签名摘要和自行计算的内容摘要判断signedBytesUnchanged,现在在signerInfo.verify(verifier)返回true时设置该值为true是合理的:因为signerInfo.verify已完成签名完整性验证,内部已确认签名对应的内容摘要与实际计算的摘要匹配,此时设置该属性完全可靠。其他组件的属性需求
无需担心冗余初始化问题:- 有签名属性的场景,
getContentDigest()返回的就是原方法从messageDigest提取的值,与之前的sigDigestBytes完全一致; - 无签名属性的场景,返回的是Bouncy Castle验证过程中生成的有效摘要,同样能满足其他组件对
checksum的需求; signedBytesUnchanged的取值完全符合原逻辑语义,不会影响依赖该属性的组件。
- 有签名属性的场景,
额外建议
如果需要完全对齐原逻辑语义(比如验签失败时也需设置checksum),当前代码中if (signerInfo.verify(verifier))分支外的signatureInfo.checksum = DatatypeConverter.printHexBinary(signerInfo.getContentDigest());可以保留,确保无论验签成功与否,checksum都能被正确初始化,满足其他组件需求。
内容的提问来源于stack exchange,提问作者Štěpán Vejvoda

