有关数字签名CMS签名和PKCS7

2023-06-05  本文已影响0人  塔塔斯坦

概念

纯技术,无任何业务信息

CMS --- Cryptographic Message Syntax
PKCS7 --- Public Key Cryptography Standards #7

PKCS是一整套的,比如PKCS#1是讲RSA算法, PKCS#8是讲私钥保密的,而PKCS#7是讲通过公私钥的加密解密实现的身份验证和数据完整性验证。

相关的几个标准:
rfc2315 PKCS #7: Cryptographic Message Syntax Version 1.5
rfc2630 Cryptographic Message Syntax
rfc5652 Cryptographic Message Syntax (CMS)

按照以上信息,PKCS#7也就是CMS 1.5, 最新的CMS rfc5652是对之前得到PKCS#7等的升级演进。是兼容PKCS#7的。
网上搜索用BouncyCastle做pkcs7签名, 实际代码中类名都有很多处CMSxxx
因此现在来说, PKCS7的签名和CMS签名实际上是一回事。

关于签名文件内容和后缀

通常签名后的文件有两种格式

二进制格式(一般后缀p7s, 也叫DER格式,用notepad++打开看不懂)和PEM格式(base64的文本)。

(类似的,证书文件也是这两种情况。)

PEM的格式签名通常有两种:

-----BEGIN PKCS7-----
MIIGWgYJKoZIhvcNAxxxxxxxxxxxxxxxxxxxxxxxx
-----END PKCS7-----
-----BEGIN CMS-----
MIIGWgYJKoZIhvcNAxxxxxxxxxxxxxxxxxxxxxxxx
-----BEGIN CMS-----

二进制格式的可以通过命令转换成PEM格式:

openssl pkcs7 -in UDM20.5.1.25_22.UpgradeTool.zip.p7s  -inform der  -out signed.p7s.pem

这几种格式实质上是一样的,至少用openssl命令,或者自己写的调用BouncyCastle的代码,或者用公司开发的CMS校验库都一样处理。

只不过有的时候调用代码库需要输入String的时候,只能用文本格式(PEM)而已。

PKCS#7签名和验证过程

签名目的是确保签名者身份,以及签名后的信息不能被篡改

签名的步骤包括:

  1. 计算原始信息的摘要值(根据指定的摘要算法)

  2. 用RSA(或DSA等其他非对称算法)的私钥加密这个摘要。

签名验证的步骤:

  1. 用RSA公钥解密得到一个摘要值
  2. 用相同的摘要算法,对原始信息计算也得到一个摘要值
  3. 对比1和2的结果,如果相同,验证通过。

签名和验签操作:

Openssl中的cms签名和校验

#自己写的一对一签名和校验。
openssl cms -sign -nosmimecap -signer sig.crt -inkey sig.pem -binary -in source.txt -out dest.txt
openssl cms -verify -in dest.txt -signer sig.crt -CAfile root.crt -out signedtext.txt

验证已经签发好的软件包

目前来看,openssl命令、自己写的代码和我们调用公司CMS签名库的方法,这几种都是一致的,只要一个能校验通过,其他也都可以。

openssl cms命令验证

openssl cms -verify -binary -in signed_file.p7s  -signer "ca.der"  -inform der   -noverify  -content content.zip    -certsout mycerts.pem > /dev/null

举例常见错:
openssl校验失败,报错如下, 同时在java中读取的CMSSignedData对象.getSignedContent()为空,是因为缺少原文内容,需加参数-content。

[root@dggphisprd47503 package_ok]# openssl cms -verify  -in signed.cms  -signer public.pem -inform PEM   -noverify

Verification failure
139927364507536:error:2E06307F:CMS routines:CHECK_CONTENT:no content:cms_smime.c:120:

其他

  从p7s文件中分离出签名所用的证书(可能只支持p7s的二进制和pem,不支持cms,待验证)
  ```shell
  openssl pkcs7 -in signed.p7s  -inform pem -print_certs  


###  BouncyCastle代码验证


```java
public PKCS7SignTool() {
        if (Security.getProvider("BC") == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    //只验证一个签名, 
    public static boolean verify(String certFile, byte[] originMessage, byte[] signedMessage)
        throws FileNotFoundException, CertificateException, CMSException, OperatorCreationException {
        Certificate certificate = loadCert(certFile);
    
        CMSSignedData sign = new CMSSignedData(new CMSProcessableByteArray(originMessage), Base64.decode(signedMessage));
    
        CollectionStore<X509CertificateHolder> certificateHolderStore = (CollectionStore)sign.getCertificates();
        for (Iterator i = certificateHolderStore.iterator(); i.hasNext(); ) {
            X509Certificate x509Cert = new JcaX509CertificateConverter().getCertificate((X509CertificateHolder) i.next());
            //System.out.println("cert in signedMsg: "+x509Cert.getSubjectDN()+x509Cert.getSerialNumber());
        }
    
        SignerInformationStore signers = sign.getSignerInfos();
        SignerInformation signerInfo = signers.getSigners().iterator().next();
        //这里证书使用了传入的证书,没有用签名文件中的证书。实际正常都要用到的。
        PublicKey publicKey = certificate.getPublicKey();
        return signerInfo.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider("BC").build(publicKey));
    }
    
    public static byte[] sign(String certFile, String keyFile, byte[] srcMessage,boolean containCert)
        throws IOException, CertificateException, CMSException, NoSuchAlgorithmException, InvalidKeySpecException,
               OperatorCreationException {
        Certificate certificate = loadCert(certFile);
    
        byte[] encodedKey = Files.readAllBytes(Paths.get(keyFile));
        String keyStr = new String(encodedKey).replace("-----BEGIN RSA PRIVATE KEY-----", "")
            .replace("-----END RSA PRIVATE KEY-----", "");
    
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.decode(keyStr.getBytes()));
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = kf.generatePrivate(spec);


        ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(privateKey);
    
        CMSSignedDataGenerator generator  = new CMSSignedDataGenerator();
        generator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(
            new JcaDigestCalculatorProviderBuilder().setProvider("BC")
                .build()).build(signer,  (X509Certificate)certificate));
    
        //add cert to generated sign data;
        if(containCert){
            generator.addCertificates(new JcaCertStore(Arrays.asList(certificate)));
        }
    
        CMSSignedData signedData = generator.generate(new CMSProcessableByteArray(srcMessage), false);
        return Base64.encode(signedData.getEncoded());
    }
    
    private static Certificate loadCert(String certFile) throws FileNotFoundException, CertificateException {
        InputStream inStream = new FileInputStream(certFile);
        BufferedInputStream bis = new BufferedInputStream(inStream);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        Certificate certificate = cf.generateCertificate(bis);
        return certificate;
    }

注意证书的key usage

上述操作中,openssl验证签名时同时要验证证书(除非增加参数 -noverify),
BouncyCastle的接口中不会验证证书,对证书的验证要自行另外写代码。
通常证书验证仅包含有效期,签发者是否可信等等等,
如果证书做签名和验证签名,实际上还有一个点,很容易遗漏的,就是证书的key usage。
通过命令可以查看证书用途。

openssl x509  -in  xxx.crt  -text

根据https://tools.ietf.org/html/rfc5280#section-4.2.1.3的说明,如果要做书签签名,keyusage要包含digitalSignature (0)

如果没有包含的话, openssl验证签名时不通过的, 会报错Verify error:unsupported certificate purpose。

之前给友商技术人员解释,类似于你有一个正规的驾照,只规定了能开C1汽车, 如果用来开摩托车是不行的。
这一点严格的说要校验出来,但是大部分时候都被忽略了, 以后需关注。

当然自己写校验代码或者有openssl时通常都忽略掉了这个校验。




# 签名不通过问题的校验步骤

1. 首先确认是不是真的校验不通过。可以要求问题提出方用openssl命令校验一下。

 校验时可以先不考虑证书链(输入参数noverify)

 - openssl命令目前看是完全一致的,只要是我们系统应当通过的, openssl都可以通过。

 - 我们也可以协助用命令校验,如果还是不通过,就不用往下走了。

 - 如果对方无法提供openssl的校验证明,能提供校验代码也可以,否则的话,他如何说明制作的包是符合规范的呢。

 - 如果确定校验没有问题,把相关的原文,签名文件,证书都拿过来,在家里linux环境用openssl再确认一遍。

2. 手工接触签名文件中的证书, 看和提供的证书加到一起,能否从自签CA到最终证书能否构成一条链。

3. 有了以上保证,将openssl中校验通过的content输入,去掉begin和end头尾作为字符串作为原文,和签名文件一起输入我们系统中,必然是能校验通过的。

 此时就对比前台传入包时和我们自己构造的文件的差别即可定位。

上一篇下一篇

猜你喜欢

热点阅读