基于自签名的 X.509 数字证书生成及验证
数字证书用于标志网络用户身份,在 Web 应用中,数字证书的应用十分广泛,如:安全电子邮件、访问安全站点、安全电子事务处理和安全电子交易等。
数字证书的格式一般采用 X.509 国际标准。目前,数字证书认证中心主要签发安全电子邮件证书、个人和企业身份证书、服务器证书以及代码签名证书等几种类型证书。
数字证书由证书机构签发,证书机构通常需经权威认证机构注册认证。在企业应用中,也常用企业自身作为发证机构(未经过认证)签发数字证书,证书的使用范围也常是企业内部,这样的证书就是所谓的“自签名”的。
数字证书采用公钥密码体制,每个用户拥有一把仅为本人所掌握的私有密钥 ( 私钥 ) ,用它进行解密和签名 ; 同时拥有一把公共密钥 ( 公钥 ) 并可以对外公开,用于加密和验证签名。
下面介绍我们在项目中常用的自签名证书的生成及验证方法。为简单起见,我们假设所有网站用户使用同一个数字证书 clientCA 。
一、 服务器端
登录远程服务器,在服务器端生成证书并提供下载。服务器上应当安装 jdk1.5 以上,因为我们需要使用 jdk 自带的 keytool 工具, keytool 工具位于 jdk 安装目录的 bin 目录下。
1、 生成密钥数据库及根证书
如果是第 1 次使用数字证书,那么很可能服务器上还没有密钥数据库。我们可以使用下列命令成密钥数据库( keystore )。
keytool -genkey -dname "CN= 发证人姓名 ,OU= 发证人所属部门 ,O= 发证人所属公司 ,L= 昆明市 ,ST= 云南省 ,C= 中国 " -alias IPCCCA -keyalg RSA -keysize 1024 -keystore IPCCCA -keypass 根证书密码 -storepass 库密码
运行脚本后,会在当前用户主目录(如: C:/Documents and Settings/Administrator 目录, window 系统)下生成密钥数据库文件 IPCCCA 。注意,需要设置两个密码,一个是根证书密码 keypass ,一个是库密码 storepass ,如果你不是很确定二者间的区别,最好两个都设置成一样。
2、 生成自签名证书
运行下列命令,生成一个自签名证书:
keytool -genkey -dname "CN= 发证人姓名 ,OU= 发证人所属部门 ,O= 发证人所属公司 ,L= 昆明市 ,ST= 云南省 ,C= 中国 " -alias clientCA -keyalg RSA -keysize 1024 -keystore IPCCCA -keypass 123456 -storepass 库密码 -validity 1
这个证书是一个自签名证书,该证书的别名为 clientCA ,存储在前面生成的那个密钥库文件 (IPCCCA) 中。这需要提供访问该库的库密码 (storepass) ,必须跟第 1 步中的一样。 Kepass 是该证书的公钥,验证证书时需要提供该密钥。
并且为了便于测试,我们把证书有效期设置为 1 天。这样每过一天,用户必须重新下载证书。
3、 查看密钥库
你可以用下列命令查看生成的两个密钥:
keytool -list -keystore IPCCCA –storepass 库密码
结果会列出两个密钥,类似如下:
您的 keystore 包含 2 输入
clientca, 2011-3-30, keyEntry,
认证指纹 (MD5) : 10:B8:51:54:7B:1C:60:7C:89:E7:B6:8E:71:E5:E1:E7
ipccca, 2011-3-30, keyEntry,
认证指纹 (MD5) : C3:E3:7D:7C:9B:AA:05:84:92:AF:93:18:42:D2:1C:07
4、 提供证书下载
我们可以在服务器上放一个 servlet ,以提供自签名证书的下载:
private static final long serialVersionUID = 1L;
// 有效期天数
private static final int Max_Days = 1;
// keystore 密码
private static final char[] password = "ipcc@95598".toCharArray();
// keystore 文件路径
private static final String keystoreFilename = "C://Documents and Settings//Administrator//IPCCCA";
// 证书文件名
private static final String certFilename="client.cer";
// 证书别名
private static final String alias = "clientCA";
private KeyStore keystore;
private String sigAlgrithm;
// 读取 keystore
private KeyStore loadKeystore(String keystorepath) {
KeyStore ks = null;
try {
FileInputStream fIn = new FileInputStream(keystorepath);
ks = KeyStore.getInstance("JKS");
ks.load(fIn, password);
fIn.close();
return ks;
} catch (Exception e) {
System.out.println(e.getMessage());
}
return ks;
}
// 获得 CertInfo
private X509CertInfo getCertInfo(Certificate c, String alias) {
X509CertInfo certInfo = null;
try {
// 从待签发的证书中提取证书信息
byte[] encod2 = c.getEncoded();// 获取 证书内容(经过编码的字节)
X509CertImpl cimp2 = new X509CertImpl(encod2);// 创建 X509CertImpl 象
sigAlgrithm=cimp2.getSigAlgName();
// 获取 X509CertInfo 对象
certInfo = (X509CertInfo) cimp2.get(X509CertImpl.NAME
+ "." + X509CertImpl.INFO);
} catch (Exception e) {
System.out.println(e.getMessage());
}
return certInfo;
}
// 修改有效期
private void updateValidity(X509CertInfo cinfo, int days) {
// 获取当前时间
Date d1 = new Date();
// 有效期为当前日期后延 n 天
Date d2 = new Date(d1.getTime() + days * 24 * 60 * 60 * 1000L);
// 创建有效期对象
CertificateValidity cv = new CertificateValidity(d1, d2);
try {
cinfo.set(X509CertInfo.VALIDITY, cv);// 设置有效期
} catch (Exception e) {
e.printStackTrace();
}
}
// 存储证书
private void saveCert(KeyStore ks, char[] storepass, String alias,
PrivateKey pKey, char[] keypass, X509CertInfo cinfo,String algrithm) {
try {
X509CertImpl cert = new X509CertImpl(cinfo);// 新建证书
cert.sign(pKey, algrithm); // 使用 CA 私钥对其签名
// 获取别名对应条目的证书链
Certificate[] chain = new Certificate[] { cert };
// 向密钥库中添加条目 , 使用已存在别名将覆盖已存在条目
ks.setKeyEntry(alias, pKey, keypass, chain);
// 将 keystore 存储至文件
FileOutputStream fOut = new FileOutputStream(keystoreFilename);
keystore.store(fOut, password);
fOut.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 导出证书
private void exportCert(KeyStore ks,String alias,HttpServletResponse response){
try{
Certificate cert = keystore.getCertificate(alias);
// 得到证书内容(以编码过的格式)
byte[] buf = cert.getEncoded();
// 写证书文件
response.setContentType("application/x-download");
response.addHeader("Content-Disposition", "attachment;filename="
+ certFilename);
OutputStream out = response.getOutputStream();
out.write(buf);
out.close();
}catch(Exception e){
e.printStackTrace();
}
}
/**
* @see HttpServlet#HttpServlet()
*/
public GetNewCert() {
super();
// TODO Auto-generated constructor stub
}
/**
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try{
keystore = loadKeystore(keystoreFilename); // 读取 keystore
Certificate c = keystore.getCertificate(alias);// 读取证书
X509CertInfo cinfo = getCertInfo(c, alias);// 获得证书的 CertInfo
updateValidity(cinfo, Max_Days);// 修改证书有效期
// 从密钥库中读取 CA 的私钥
PrivateKey pKey = (PrivateKey) keystore.getKey(alias, "123456"
.toCharArray());
// 将 keystore 存储至 keystore 文件
saveCert(keystore, password, alias, pKey, "123456".toCharArray(),cinfo,sigAlgrithm);
exportCert(keystore,alias,response);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request,response);
}
}
这个 servlet 的作用是:
当用户请求该 serlvet ,从密钥库中提取 clientCA 证书,将证书有效期修改为当前日期到下一天。
也就是说,当用户客户端请求该 servlet ,即可获得一个新的证书,这个证书的有效期已经向后延了一天。
我们的目的是,用户每次登录后,检查用户下载的证书,如证书已过了有效期,则请求此 servlet 即可获得一个有效的新证书。
二、 客户端
客户端可能是任何设备,包括 pc 、移动终端。我们假设客户端是基于 Android1.6 以上的移动终端,则以下是 java 客户端的证书验证类 MyCertificate :
public class MyCertificate {
private static String tag="MyCertificate";
public static Certificate readCert(File file){
Certificate c=null;
try{
CertificateFactory cf = CertificateFactory.getInstance("X.509");
FileInputStream in1 = new FileInputStream(file);
c = cf.generateCertificate(in1);
in1.close();
} catch (Exception e) {
Log.e(tag, e.toString());
}
return c;
}
// 验证证书的有效性
public static boolean verifyCert(Certificate c){
PublicKey pbk=c.getPublicKey();
try{
c.verify(pbk);
return true;
}catch(Exception e){
Log.e(tag,"Certificate is invalid");
}
return false;
}
// 验证证书有效期
public static int verifyCertValidity(Date date,Certificate c){
int i=0;
X509Certificate t=(X509Certificate)c;
try {// 有效
t.checkValidity(date);
} catch (CertificateExpiredException e) {// 过期
Log.e(tag,"Certificate Expired");
i=-1;
} catch (CertificateNotYetValidException e) {// 尚未生效
Log.e(tag,"Certificate Too early");
i=-2;
}
return i;
}
public static boolean verify(Context ctx){
Activity act=(Activity)ctx;
boolean b=false;
// 检查证书文件是否存在
File file=new File(Environment.getExternalStorageDirectory()+act.getString(R.string.CERT_DIR)+act.getString(R.string.CERT_FILE));
if(!file.exists()){
act.showDialog(1);
}else{
Date d=new Date();// 取当前时间
Certificate c=MyCertificate.readCert(file);// 读取证书文件
// 校验证书有效性
if(!MyCertificate.verifyCert(c)){
act.showDialog(0);// 无效证书
}else{
// 校验证书有效期
int i=MyCertificate.verifyCertValidity(d,c);
switch(i){
case 0:// 有效
b=true;
break;
case -1:// 过期
act.showDialog(2);
break;
case -2:// 未生效
act.showDialog(3);
break;
}
}
}
return b;
}
}
在相关 activity 中可以这样使用它:
private void login(String acc, String pass) {
String url = this.getString(R.string.PORT_LOGIN_URL);
url = String.format(url, acc, pass);
// Log.i(tag,url);
MainLoginHandler handler = new MainLoginHandler();
modules = SaxHelper.getModules(url, handler);
// Log.i(tag,systems.toString());
Log.i("modules:", "" + modules);
if (modules != null) {
String status = (String) modules.get("loginstatus");
if ("true".equals(status)) {// 登录成功
if (!verifyCert()) {
return;
}
Bundle bundle = new Bundle();
bundle.putSerializable("data",
(Serializable) modules.get("modules"));
gotoActivity(main.class, bundle);
} else {
Toast.makeText(getBaseContext(), " 用户名或密码错误! ",
Toast.LENGTH_SHORT).show();
}
}
}
private boolean verifyCert() {
return MyCertificate.verify(this);
}
// 创建 activity 托管对话框
protected Dialog onCreateDialog(int id) {
Log.e("::::", "showdialog!");
String msg = "";
switch (id) {
case 1:
msg = " 证书未下载!请点击“是”以下载证书。 ";
break;
case 2:
msg = " 证书已过期!请点击“是”重新下载证书。 ";
break;
case 3:
msg = " 证书尚未生效!请等证书生效后再重新登录。 ";
// 对于未生效的证书,无需重新下载,等证书生效即可
return new AlertDialog.Builder(this)
.setMessage(msg)
.setNegativeButton(" 是 ",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();// removeDialog(0); 移除对话框
}
}).create();
case 4:
return new AlertDialog.Builder(this)
.setMessage(" 位置源未设置!是否现住设置位置源? ")
.setPositiveButton(" 是 ",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
// 转至 GPS 设置界面
Intent intent = new Intent(
Settings.ACTION_SECURITY_SETTINGS);
startActivityForResult(intent, 0);
}
})
.setNegativeButton(" 不 ",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();// removeDialog(0); 移除对话框
}
}).create();
default:
msg = " 无效的证书!请点击“是”重新下载证书。 ";
}
return new AlertDialog.Builder(this).setMessage(msg)
.setPositiveButton(" 是 ", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 开始下载证书
downloadCert();
}
})
.setNegativeButton(" 不 ", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();// removeDialog(0); 移除对话框
}
}).create();
}
红色加粗部分的代码调用了 MyCertificate.verify() 。 onCreateDialog 方法则通过对话框方式返回证书校验的结果。