有关数字签名CMS签名和PKCS7
概念
纯技术,无任何业务信息
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签名和验证过程
签名目的是确保签名者身份,以及签名后的信息不能被篡改
签名的步骤包括:
-
计算原始信息的摘要值(根据指定的摘要算法)
-
用RSA(或DSA等其他非对称算法)的私钥加密这个摘要。
签名验证的步骤:
- 用RSA公钥解密得到一个摘要值
- 用相同的摘要算法,对原始信息计算也得到一个摘要值
- 对比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头尾作为字符串作为原文,和签名文件一起输入我们系统中,必然是能校验通过的。
此时就对比前台传入包时和我们自己构造的文件的差别即可定位。