"Almost no one will remember what he had just not interested." - Nobody
“幾乎沒有人會記得他所絲毫不感興趣的事情。” —— 佚名
0x00 大綱
目錄- 0x00 大綱
- 0x01 前言
- 0x02 技術選型
-
0x03 需求分析
- 目標用戶
- 用戶故事
- 功能需求
- 安全需求
- 兼容需求
- 性能需求
-
0x04 原型設計
- 主窗體
- RSA根證書生成頁面
- ECC根證書生成頁面
- RSA證書生成頁面
- ECC證書生成頁面
- 0x05 架構設計
- 0x06 概要設計
- 0x07 詳細設計
-
0x08 編碼實現
- RSA根證書生成(節選關鍵部分)
- ECC根證書生成
-
0x09 小結
- 運行效果
0x01 前言
隨著這些年人們對于信息安全的愈加重視,工作中經常用到各種數字證書,最主要的用途就是加密和簽名。但是反復生成用于測試的數字證書是一件麻煩事,雖然感謝有強大的OpenSSL工具,但是問就是記不住命令,問就是不想看文檔。為了提高工作效率,降低苦痛指數,遂決定開發一個證書生成小工具。
0x02 技術選型
我們需要一個簡單的GUI,在Java的領域中,可以選擇使用JavaFX或著Swing來實現,鑒于我們不需要太花哨的東西,只需要幾個簡單的布局和控件,這里選擇了使用Swing來開發圖形界面。小聲BB:甲方是我,乙方也是我,能用就行,要啥自行車。
我們需要用到RSA或著國密的數字證書,湊巧BouncyCastle就是一個提供了很多哈希算法和加密算法的第三方庫。老牌子,易使用。
初步估計工期為一周,太長容易從入門到放棄,太短則對心臟不友好。
0x03 需求分析
目標用戶
- 給開發人員用
- 給業務人員用
- 給測試人員用
用戶故事
作為使用者:
- 我需要工具同時支持生成RSA和國密的證書;
- 我想簡單點幾下按鈕就能生成自己想要的證書,我不想記一大串又臭又長的命令行;
- 我不希望到處搜索一堆奇奇怪怪的參數,最好什么都不用我填,什么都不用我自己思考;
- 什么YES or NO統統不要,問就是YES......
- 我希望你界面盡量不要太花哨,時間長了看的我眼花;
- 最好支持批量操作,免得折了我鼠標的壽。
功能需求
- 使用GUI操作的小工具,包含四個功能菜單:
- RSA根證書生成
- RSA證書生成
- ECC根證書生成,支持批量操作;
- ECC證書生成,支持批量操作;
- 生成的同時通過文件選擇對話框選擇保存路徑;
- RSA根證書簽發:
- 可以根據選定的密鑰算法和簽名算法,生成RSA和國密的非對稱密鑰對,默認使用123456對私鑰進行加密,可指定密碼,也可一鍵生成隨機密碼;
- 證書
- 對于同一對密鑰,需要同時導出:
- DER編碼的PKCS#8加密私鑰,文件后綴名為.key;
- PEM編碼的PKCS#8加密私鑰,文件后綴名為.pri.pem;
- PEM編碼的X.509公鑰,文件后綴名為.pub.pem;
- DER編碼的X.509公鑰,文件后綴名為.cer;
- 同時包含X.509公鑰和PKCS#8加密私鑰的PKCS#12證書,文件后綴名為.pfx;
- 包含私鑰密碼的文本文件,文件后綴為.txt;
以上文件在一個壓縮包中打包導出,壓縮文件后綴為.zip;
- RSA證書簽發:
- 使用選定的根證書,簽發中間證書或葉子證書;
- 導出要求同上;
- ECC證書簽發:
- 同RSA根證書簽發;
- ECC證書簽發:
- 使用國密sm2p256v1曲線,其余同RSA證書簽發;
- 對于RSA證書,擬支持2048(首選)、1024、4096密鑰位數,支持SHA1withRSA、SHA256withRSA、MD5withRSA簽名算法;
- 對于ECC證書,擬支持sm2p256v1曲線,支持SM3withSM2、SHA256withSM2簽名算法;
- 證書頒發者信息至少包含:
- country code (C)
- locality name (L)
- state/province name (ST)
- organization (O)
- common name (CN)
- organizational unit name (OU)
最好都有默認值,能不填就盡量不要填,免得浪費我喝咖啡的時間;
- 證書使用者信息默認同頒發者信息;
- 證書有效期默認十年(部分系統不允許過長的證書有效期,第一個版本不考慮這么多);
安全需求
略,見功能需求加密部分。
兼容需求
應與操作系統和其他軟硬加密機生成的證書互相兼容。
性能需求
能用就行。
0x04 原型設計
有了大體上的功能需求分析,就可以進入設計階段了,既然是GUI程序,那就先從界面入手,簡單做一下原型。正好idea上帶有Swing UI Designer,所見即所得,原型可以直接轉化為后期工程。
主窗體
使用JTabbedPane作為容器,承載四個主要功能菜單,簡化交互設計。再加上四個功能界面功能上高度相似,也意味著GUI元素的高度相似,只要設計出一個,其他幾個直接Ctrl+C,Ctrl+V即可。
為了減少動態窗體大小帶來的布局調整和元素Resize,直接使用固定大小窗體,初步定為 640*560 像素。
RSA根證書生成頁面
大部分GUI元素都是按鈕(JButton),下拉框(JComboBox)和輸入框(JTextField or JPasswordField),因此可以考慮左邊為標簽(JLabel),右邊為交互元素的表單結構,首選布局即為Grid Layout。剩下的就是往JPanel上扔控件了。經過一番努力,我們得到了這樣的窗體設計:
于是通過復制粘貼,很快可以完成剩余的三個界面:
ECC根證書生成頁面
RSA證書生成頁面
ECC證書生成頁面
0x05 架構設計
由于證書生成工具的定位是C端程序,區別于B/S模型的應用,它更適合使用傳統老三層模型(USL、BLL、DAL),而不是Web應用上經常使用的MVC分層模式。
通過前面的原型設計,已經基本完成了USL表示層中的視圖部分,接下來需要實現USL的控制部分和BLL層的業務邏輯(其實就是實現各個控件需要關注的事件監聽以及對應的事件處理),由于我們的數據持久方式就是文件,所以數據訪問層反而比較簡單了。
DAL層雖然簡單,但是我們保持開放修改的后路,萬一哪天要用數據庫呢?嘿嘿~
0x06 概要設計
通過需求分析和原型設計已經大致可以劃分出功能模塊了,核心部分就是幾個按鈕的事件監聽和處理。
它們都可以統合到一個流程模型中:
參數錄入->點擊按鈕->獲取參數->驗證參數->生成數據->更新UI
以生成RSA根證書處理流程為例:
- 參數錄入:界面上選擇密鑰位數、簽名算法、證書密碼(可選)、證書頒發者信息(可選);
- 點擊按鈕:監聽按鈕點擊事件;
- 獲取參數:獲取UI和后臺的相關參數;
- 驗證參數:進行參數合法性檢查,包括但不限于長度,格式,關聯性等;
- 生成數據:根據參數,調用BouncyCastle庫生成RSA密鑰對;適配和打包輸出壓縮文件;
- 更新UI:提示生成結果,選擇文件存儲路徑等;
異常處理:
- 界面彈出提示框;
- 后臺記錄日志等;
0x07 詳細設計
將主業務流程的各個節點展開并詳細描述其實現,必要時借助偽代碼進行邏輯表示;這里就不寫了,問就是懶……
0x08 編碼實現
RSA根證書生成(節選關鍵部分)
public final class RSA {
public static final String MD5_WITH_RSA = "MD5withRSA";
public static final String SHA1_WITH_RSA = "SHA1withRSA";
public static final String SHA256_WITH_RSA = "SHA256withRSA";
public static final int KEY_SIZE_1024 = 1024;
public static final int KEY_SIZE_2048 = 2048;
public static final int KEY_SIZE_4096 = 4096;
private RSA() {
}
public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException {
if ((keySize & keySize - 1) != 0) {
throw new InvalidParameterException("非法RSA密鑰位數" + keySize);
}
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(Global.ALGO_NAME_RSA);
keyPairGenerator.initialize(keySize);
return keyPairGenerator.generateKeyPair();
}
}
private ActionListener rsaRootGenerateButtonListener() {
return e -> {
try {
checkRSARootCertificateConfig();
JFileChooser fileChooser = makeSaveFileChooser();
if (JFileChooser.APPROVE_OPTION == fileChooser.showSaveDialog(mainFrame)) {
String saveDir = fileChooser.getSelectedFile().getAbsolutePath();
char[] passwordChars = rsaRootPasswordField.getPassword();
X500Name issuer = obtainIssuerEntry(rsaIssuerCField, rsaIssuerLField, rsaIssuerSTField, rsaIssuerOField, rsaIssuerCNField, rsaIssuerOUField).getValue();
KeyPair caKeyPair = RSA.generateKeyPair(rsaRootKeySizeBox.getItemAt(rsaRootKeySizeBox.getSelectedIndex()));
X509Certificate caCertificate = CertificateUtil.generateCertificate(rsaRootSignAlgoBox.getItemAt(rsaRootSignAlgoBox.getSelectedIndex()), caKeyPair, issuer);
caCertificate.checkValidity(caCertificate.getNotBefore());
caCertificate.checkValidity(caCertificate.getNotAfter());
caCertificate.verify(caKeyPair.getPublic());
PKCS8EncryptedPrivateKeyInfo caPrivateKeyInfo = KeyUtil.makePKCS8EncryptedKey(caKeyPair.getPrivate(), passwordChars);
PKCS12PfxPdu caPfxPdu = KeyUtil.makePfx(caCertificate, caKeyPair.getPrivate(), passwordChars);
saveCertificateFiles(saveDir, rsaIssuerCNField.getText(), caPrivateKeyInfo, caCertificate, caPfxPdu, passwordChars);
}
} catch (RuntimeException | IOException | GeneralSecurityException | OperatorCreationException | PKCSException ex) {
JOptionPane.showMessageDialog(mainFrame, ex.getLocalizedMessage(), "錯誤提示", JOptionPane.ERROR_MESSAGE);
}
};
}
public static X509Certificate generateCertificate(String signatureAlgorithm, KeyPair keyPair, X500Name subject) throws OperatorCreationException, IOExcepti
return generateCertificate(signatureAlgorithm, keyPair, subject, Duration.ofDays(365L * 10));
}
public static X509Certificate generateCertificate(String signatureAlgorithm, KeyPair keyPair, X500Name subject, Duration duration) throws OperatorCreationE
ContentSigner contentSigner = makeContentSigner(signatureAlgorithm, keyPair.getPrivate());
PKCS10CertificationRequest csr = generateCsr(contentSigner, keyPair, subject, true);
BcX509ExtensionUtils extUtils = new BcX509ExtensionUtils();
final Date notBefore = new Date();
X509v3CertificateBuilder v3CertificateBuilder = new X509v3CertificateBuilder(
csr.getSubject(),
BigInteger.valueOf(UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE),
notBefore,
new Date(notBefore.getTime() + duration.toMillis()),
Locale.CHINA,
csr.getSubject(),
csr.getSubjectPublicKeyInfo());
v3CertificateBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
v3CertificateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
v3CertificateBuilder.addExtension(Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(csr.getSubjectPublicKeyInfo()));
v3CertificateBuilder.addExtension(Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(csr.getSubjectPublicKeyInfo()));
return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(v3CertificateBuilder.build(contentSigner));
}
public static PKCS8EncryptedPrivateKeyInfo makePKCS8EncryptedKey(PrivateKey privateKey, char[] passwordChars) {
PKCS8EncryptedPrivateKeyInfoBuilder privateKeyInfoBuilder = new JcaPKCS8EncryptedPrivateKeyInfoBuilder(privateKey);
return privateKeyInfoBuilder.build(new BcPKCS12PBEOutputEncryptorBuilder(
PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC,
new CBCBlockCipher(new DESedeEngine())).build(passwordChars));
}
public static PKCS12PfxPdu makePfx(X509Certificate x509Certificate, PrivateKey privateKey, char[] passwordChars) throws IOException, PKCSException {
PKCS12SafeBagBuilder certBagBuilder = new JcaPKCS12SafeBagBuilder(x509Certificate);
certBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString("Forge CA Certification"));
certBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId, ASN1Primitive.fromByteArray(x509Certificate.getExtensionValue(Extension.subjectKeyIdentifier.getId())));
PKCS12SafeBagBuilder keyBagBuilder = new JcaPKCS12SafeBagBuilder(privateKey,
new BcPKCS12PBEOutputEncryptorBuilder(
PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC,
new CBCBlockCipher(new DESedeEngine())).build(passwordChars));
keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString("Forge CA Key"));
keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId, ASN1Primitive.fromByteArray(x509Certificate.getExtensionValue(Extension.subjectKeyIdentifier.getId())));
PKCS12PfxPduBuilder pfxPduBuilder = new PKCS12PfxPduBuilder();
PKCS12SafeBag[] certs = new PKCS12SafeBag[1];
certs[0] = certBagBuilder.build();
pfxPduBuilder.addEncryptedData(new BcPKCS12PBEOutputEncryptorBuilder(
PKCSObjectIdentifiers.pbeWithSHAAnd40BitRC2_CBC,
new CBCBlockCipher(new RC2Engine())).build(passwordChars),
certs);
pfxPduBuilder.addData(keyBagBuilder.build());
return pfxPduBuilder.build(new BcPKCS12MacCalculatorBuilder(), passwordChars);
}
ECC根證書生成
略,與RSA大同小異。
0x09 小結
運行效果
完成任務。