概念
純技術(shù)约计,無(wú)任何業(yè)務(wù)信息
CMS --- Cryptographic Message Syntax
PKCS7 --- Public Key Cryptography Standards #7
PKCS是一整套的嫌拣,比如PKCS#1是講RSA算法杨刨, PKCS#8是講私鑰保密的,而PKCS#7是講通過(guò)公私鑰的加密解密實(shí)現(xiàn)的身份驗(yàn)證和數(shù)據(jù)完整性驗(yàn)證浪南。
相關(guān)的幾個(gè)標(biāo)準(zhǔn):
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是對(duì)之前得到PKCS#7等的升級(jí)演進(jìn)斩松。是兼容PKCS#7的伶唯。
網(wǎng)上搜索用BouncyCastle做pkcs7簽名, 實(shí)際代碼中類名都有很多處CMSxxx
因此現(xiàn)在來(lái)說(shuō)惧盹, PKCS7的簽名和CMS簽名實(shí)際上是一回事乳幸。
關(guān)于簽名文件內(nèi)容和后綴
通常簽名后的文件有兩種格式
二進(jìn)制格式(一般后綴p7s, 也叫DER格式,用notepad++打開(kāi)看不懂)和PEM格式(base64的文本)钧椰。
(類似的粹断,證書文件也是這兩種情況。)
PEM的格式簽名通常有兩種:
-----BEGIN PKCS7-----
MIIGWgYJKoZIhvcNAxxxxxxxxxxxxxxxxxxxxxxxx
-----END PKCS7-----
-----BEGIN CMS-----
MIIGWgYJKoZIhvcNAxxxxxxxxxxxxxxxxxxxxxxxx
-----BEGIN CMS-----
二進(jìn)制格式的可以通過(guò)命令轉(zhuǎn)換成PEM格式:
openssl pkcs7 -in UDM20.5.1.25_22.UpgradeTool.zip.p7s -inform der -out signed.p7s.pem
這幾種格式實(shí)質(zhì)上是一樣的嫡霞,至少用openssl命令瓶埋,或者自己寫的調(diào)用BouncyCastle的代碼,或者用公司開(kāi)發(fā)的CMS校驗(yàn)庫(kù)都一樣處理秒际。
只不過(guò)有的時(shí)候調(diào)用代碼庫(kù)需要輸入String的時(shí)候悬赏,只能用文本格式(PEM)而已。
PKCS#7簽名和驗(yàn)證過(guò)程
簽名目的是確保簽名者身份娄徊,以及簽名后的信息不能被篡改
簽名的步驟包括:
計(jì)算原始信息的摘要值(根據(jù)指定的摘要算法)
用RSA(或DSA等其他非對(duì)稱算法)的私鑰加密這個(gè)摘要闽颇。
簽名驗(yàn)證的步驟:
- 用RSA公鑰解密得到一個(gè)摘要值
- 用相同的摘要算法,對(duì)原始信息計(jì)算也得到一個(gè)摘要值
- 對(duì)比1和2的結(jié)果寄锐,如果相同兵多,驗(yàn)證通過(guò)尖啡。
簽名和驗(yàn)簽操作:
Openssl中的cms簽名和校驗(yàn)
#自己寫的一對(duì)一簽名和校驗(yàn)。
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
驗(yàn)證已經(jīng)簽發(fā)好的軟件包
目前來(lái)看剩膘,openssl命令衅斩、自己寫的代碼和我們調(diào)用公司CMS簽名庫(kù)的方法,這幾種都是一致的怠褐,只要一個(gè)能校驗(yàn)通過(guò)畏梆,其他也都可以。
openssl cms命令驗(yàn)證
openssl cms -verify -binary -in signed_file.p7s -signer "ca.der" -inform der -noverify -content content.zip -certsout mycerts.pem > /dev/null
- binary參數(shù)表示對(duì)輸入的內(nèi)容直接使用奈懒,不轉(zhuǎn)換處理奠涌。(否則的話,除非你不提供content磷杏,如果提供了溜畅,就會(huì)處理成smime什么的內(nèi)容格式,驗(yàn)證就會(huì)有問(wèn)題)
- in表示輸入的簽名文件极祸。
- inform指定輸入的簽名文件的格式慈格。我們這里常用也就是DER或PEM。
- signer是直接簽署這個(gè)簽名用的證書遥金。也可以是個(gè)CA證書浴捆,但是簽名文件內(nèi)必須包含由該證書簽發(fā)的“直接簽名用證書”。 也就是說(shuō)必須能夠鏈起來(lái)汰规,知道最后一個(gè)證書是用來(lái)簽名的汤功。
- noverify 不對(duì)證書本身的有效性做檢查。(但還是必須鏈起來(lái))
- content 有時(shí)候簽名文件中會(huì)包含原文溜哮。但不包含的時(shí)候需要用這個(gè)參數(shù)指定原文件。
- certsout 輸出簽名用的證書色解。當(dāng)然也可以不需要茂嗓。
- nointern 不加這個(gè)參數(shù)時(shí),提供的證書和簽名文件中證書同時(shí)參與驗(yàn)簽(看能不能鏈起來(lái))科阎。加了這個(gè)參數(shù)述吸, 簽名文件中的證書會(huì)被忽略。所以一般不加比較好锣笨。
- out textdata 輸出簽名原文蝌矛, 一般也不需要
- 最后的> /dev/null, 把控制臺(tái)輸出隱藏错英。因?yàn)槿绻辛?content參數(shù)入撒, 會(huì)把原文打出來(lái),內(nèi)容太多椭岩。
舉例常見(jiàn)錯(cuò):
openssl校驗(yàn)失敗茅逮,報(bào)錯(cuò)如下, 同時(shí)在java中讀取的CMSSignedData對(duì)象.getSignedContent()為空璃赡,是因?yàn)槿鄙僭膬?nèi)容,需加參數(shù)-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的二進(jìn)制和pem碉考,不支持cms,待驗(yàn)證)
```shell
openssl pkcs7 -in signed.p7s -inform pem -print_certs
### BouncyCastle代碼驗(yàn)證
```java
public PKCS7SignTool() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
//只驗(yàn)證一個(gè)簽名挺身,
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();
//這里證書使用了傳入的證書侯谁,沒(méi)有用簽名文件中的證書。實(shí)際正常都要用到的章钾。
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驗(yàn)證簽名時(shí)同時(shí)要驗(yàn)證證書(除非增加參數(shù) -noverify),
BouncyCastle的接口中不會(huì)驗(yàn)證證書伍玖,對(duì)證書的驗(yàn)證要自行另外寫代碼嫩痰。
通常證書驗(yàn)證僅包含有效期,簽發(fā)者是否可信等等等窍箍,
如果證書做簽名和驗(yàn)證簽名串纺,實(shí)際上還有一個(gè)點(diǎn),很容易遺漏的椰棘,就是證書的key usage纺棺。
通過(guò)命令可以查看證書用途。
openssl x509 -in xxx.crt -text
根據(jù)https://tools.ietf.org/html/rfc5280#section-4.2.1.3的說(shuō)明邪狞,如果要做書簽簽名祷蝌,keyusage要包含digitalSignature (0)
如果沒(méi)有包含的話, openssl驗(yàn)證簽名時(shí)不通過(guò)的帆卓, 會(huì)報(bào)錯(cuò)Verify error:unsupported certificate purpose巨朦。
之前給友商技術(shù)人員解釋,類似于你有一個(gè)正規(guī)的駕照剑令,只規(guī)定了能開(kāi)C1汽車糊啡, 如果用來(lái)開(kāi)摩托車是不行的。
這一點(diǎn)嚴(yán)格的說(shuō)要校驗(yàn)出來(lái)吁津,但是大部分時(shí)候都被忽略了棚蓄, 以后需關(guān)注。
當(dāng)然自己寫校驗(yàn)代碼或者有openssl時(shí)通常都忽略掉了這個(gè)校驗(yàn)碍脏。
# 簽名不通過(guò)問(wèn)題的校驗(yàn)步驟
1. 首先確認(rèn)是不是真的校驗(yàn)不通過(guò)梭依。可以要求問(wèn)題提出方用openssl命令校驗(yàn)一下典尾。
校驗(yàn)時(shí)可以先不考慮證書鏈(輸入?yún)?shù)noverify)
- openssl命令目前看是完全一致的役拴,只要是我們系統(tǒng)應(yīng)當(dāng)通過(guò)的, openssl都可以通過(guò)急黎。
- 我們也可以協(xié)助用命令校驗(yàn)扎狱,如果還是不通過(guò)侧到,就不用往下走了。
- 如果對(duì)方無(wú)法提供openssl的校驗(yàn)證明淤击,能提供校驗(yàn)代碼也可以匠抗,否則的話,他如何說(shuō)明制作的包是符合規(guī)范的呢污抬。
- 如果確定校驗(yàn)沒(méi)有問(wèn)題汞贸,把相關(guān)的原文,簽名文件印机,證書都拿過(guò)來(lái)矢腻,在家里linux環(huán)境用openssl再確認(rèn)一遍。
2. 手工接觸簽名文件中的證書射赛, 看和提供的證書加到一起多柑,能否從自簽CA到最終證書能否構(gòu)成一條鏈。
3. 有了以上保證楣责,將openssl中校驗(yàn)通過(guò)的content輸入竣灌,去掉begin和end頭尾作為字符串作為原文,和簽名文件一起輸入我們系統(tǒng)中秆麸,必然是能校驗(yàn)通過(guò)的初嘹。
此時(shí)就對(duì)比前臺(tái)傳入包時(shí)和我們自己構(gòu)造的文件的差別即可定位。