美文网首页
有关数字签名CMS签名和PKCS7

有关数字签名CMS签名和PKCS7

作者: 塔塔斯坦 | 来源:发表于2023-06-05 09:31 被阅读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
    
    • binary参数表示对输入的内容直接使用,不转换处理。(否则的话,除非你不提供content,如果提供了,就会处理成smime什么的内容格式,验证就会有问题)
    • in表示输入的签名文件。
    • inform指定输入的签名文件的格式。我们这里常用也就是DER或PEM。
    • signer是直接签署这个签名用的证书。也可以是个CA证书,但是签名文件内必须包含由该证书签发的“直接签名用证书”。 也就是说必须能够链起来,知道最后一个证书是用来签名的。
    • noverify 不对证书本身的有效性做检查。(但还是必须链起来)
    • content 有时候签名文件中会包含原文。但不包含的时候需要用这个参数指定原文件。
    • certsout 输出签名用的证书。当然也可以不需要。
    • nointern 不加这个参数时,提供的证书和签名文件中证书同时参与验签(看能不能链起来)。加了这个参数, 签名文件中的证书会被忽略。所以一般不加比较好。
    • out textdata 输出签名原文, 一般也不需要
    • 最后的> /dev/null, 把控制台输出隐藏。因为如果有了-content参数, 会把原文打出来,内容太多。

    举例常见错:
    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头尾作为字符串作为原文,和签名文件一起输入我们系统中,必然是能校验通过的。
    
     此时就对比前台传入包时和我们自己构造的文件的差别即可定位。
    
    

    相关文章

      网友评论

          本文标题:有关数字签名CMS签名和PKCS7

          本文链接:https://www.haomeiwen.com/subject/kcxiedtx.html