我们说的信息安全是相对于系统安全而言的,它更侧重于加密,解密,数字签名,验证,证书等等.而系统安全主要侧重于系统本身是否有安全漏洞,如常见的由于软件设计的不完善而导致的满天飞的缓冲区溢出等等.
Java语言中负责加密功能的部件是JCE(Java Crypto Extenstion),它使用开放式的思想,可以允许用户自己编写加密算法的具体实现的模块等.这些东西被称为JCE Provider,JCE的提供者.SUN公司本身提供了一些Provider.不过我推荐使用Bouncy Castle的Provider.原因是它实现的加密算法多,连比较新的椭圆曲线(ECC)算法都有了.去http://www.bouncycastle.org/可以找到你所希望的.Bouncy Castle除了提供Provider本身以外,还包括了一个S/MIME和一个open pgp 的jar包只有Provider本身是必要的,后两个包是方便你编程而提供的.例如有了S/MIME包后,你就不再需要为诸如"加密一个字符串或者一片文章然后签名"之类的很现实的应用程序写上一大堆代码了,只要引用S/MIME包中的某几个类,很快就可以搞定.而open pgp的存在,使你在用Java编写和PGP/GPG交互的程序时方便多了.
用Java写信息安全方面的程序需要做的准备工作已经说清楚了.现在假设你已经是一个对Java语言本身比较熟悉,能够用Java写一些程序的人,并且这些该下载的jar包已经下载好了,可以正式开工了.
在所有的此类程序的开头(无论是在类的构造函数中也好,在初始化函数中也好),都要先来上这么一句:Security.addProvider(new BouncyCastleProvider());将BouncyCaslte的Provider添加到系统中,这样以后系统在运行相关程序的时候调用的就是这个Provider中的加密算法.
然后我们就可以开始开CA了.首先,作为一个CA要有自己的一对公钥和私钥,我们先要生成这么一对.使用KeyPairGenerator对象就可以了,调用KeyPairGenerator.getInstance方法可以根据要生成的密钥类型来产生一个合适的实例,例如常用的RSA,DSA等.然后调用该对象的initialize方法和generateKeyPair方法就可以产生一个KeyPair对象了.然后调用KeyPair对象中的相应方法就可以获取生成的密钥对中的公钥和私钥了.
有了公钥和私钥对以后,下面的一个很现实问题就是如何把它们储存起来.通常我们要对这些安全对象,如公钥,私钥,证书等先进行编码.编码的目的是为了把结构复杂的安全对象变成字节流以便存储和读取,如DER编码.另外,通常还把DER编码后的字节流再次进行base64编码,以便使字节流中所有的字节都变成可打印的字节.
在Java语言中,这些安全对象基本上都有getEncoded()方法.例如:
byte[] keyBytes = privateKey.getEncoded();
这样就把一个私钥进行了DER编码后的结果保存到一个byte数组中了.然后就可以把这个byte数组保存到任何介质中.如果有必要的话,可以使用BC Provider中的Base64编码解码器类进行编码,就像这样:
byte data[] = Base64.encode(keyBytes);
要从文件中读取私钥则应该这样:
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyData);
KeyFactory kfac = KeyFactory.getInstance("RSA");
privateKey = kfac.generatePrivate(spec);
这里说明一下,对RSA私钥进行编码就会自动地按照PKCS8进行.因此读取的时候将包含编码的字节数组作为PKCS8EncodedKeySpec对象的构造函数的参数就可以生成一个该类型的对象.然后创建一个密钥工厂对象就可以按照要求生成一个RSA私钥了.很显然这里的keyData应该是和上面的keyBytes内容相同.
为了提高系统的安全性,通常私钥在经过DER编码后,还会使用一个口令进行加密,然后再储存在文件系统中.在使用私钥的时候,如果没有正确的口令,是无法把私钥还原出来的.
保存证书和保存私钥类似.Certificate对象中也有一个getEncoded的方法.
这次就讲这些.大家应该可以把这些安全对象很熟练地从文件系统和内存之间来回地折腾了吧.这对以后实现CA是很重要的.下面我会讲一下证书中表示主体的方法:DN.
上面我们讲到如何生成密钥对,以及如何将诸如公钥,私钥,证书等这一类安全对象在文件系统和内存之间来回转换.这些是准备开CA的基本功,现在我们讲一下CA的基本原理以及如何使用主体名称结构DN(Distinguish Name)来表示每一个证书中的主体.
一份证书就是一个权威机构对一个主体的身份的确认的证明.即一份证书表示一个权威机构确认了一个主体就是它自己,而不是其它的冒名顶替者.主体可以是一个个人,也可以不是,例如,在需要安全服务的时候,需要为一台网站的服务器颁发证书,这种证书的主体就是一台服务器.签署证书的权威机构就叫做CA,该权威机构本身也是一个主体.权威机构通过对包含有待认证的主体的一系列信息的待签名证书"(TBS,to be signed)进行数字签名来表示该机构对它的认可.一份包含了待认证的主体的相关信息的TBS再加上CA使用自己的私钥进行签名产生的字节流放在一起,就构成了一份标准的X509证书.
一个TBS中包含了以下这些主要信息:
证书的版本,通常是3(X509v3)
证书的序列号,RFC3280中规定,每个CA必须确保它颁发的每一份证书的序列号都是唯一的,并且序列号只能使用非负整数.
签发者(CA)的主体名称,一个DN对象.
证书的有效期,表示方法是两个时间值,表示了该证书从何时开始生效,何时开始作废.
待认证的主体的主体名称,也是一个DN对象.
待认证的主体的公钥,任何安全应用在确认完证书的有效性后,就可以开始使用该主体的公钥与之进行安全通信.
如果是X509v3证书,即版本号是3的话,后面还有一个证书扩展信息字段,可以在证书里面添加一些其它的信息.
下面我们来看一下表示主体的主体名称结构:DN.这个结就构是一个属性的集合.每个属性有属性名称和属性值.它的作用就是用来表示"我是谁",也就是说,这个证书到底是谁颁发给谁的,这个证书对应的公钥是谁拥有的.
通常使用一个字符串来表示DN结构,这种字符串说明了这种结构中的各个属性的类型和值:
C=CN;S=BeiJing;L=BeiJing;O=PKU;OU=ICST;CN=wolfenstein
这里C是国家和地区代码,S和L都是地区代码,S相当于省或者州这样的级别,L相当于城市级别,O是组织机构名称,OU是次级组织机构名称,CN是主体的通用名(common name).在这里,C,S,L等等属性的类型都是相对固定的,例如C一般就是用来表示国家和地区代码,在DN结构中还可以添加一些其它类型的信息,一般也都是以"xxx=xxx"这样来表示的.
下面我们来说明如何在Java语言中构造出一个主体名称对象.
BC Provider中使用X509Name对象来表示DN,构造一个X509Name的步骤大体如下:
先构造两个vector对象,用来表示属性名称和属性值:
Vector oids = new Vector();
Vector attributes = new Vector();
然后在oids这个用来表示属性名称的vector对象中将属性名称一个一个添加进去:
oids.addElement(X509Name.C);
......
oids.addElement(X509Name.CN);
X509Name对象里面有若干常量,例如上面的X509Name.C.还有X509Name.ST等等,都可以和上面举的例子对应起来.
然后将属性值添加到attributes对象中:
attributes.addElement("CN");
......
attributes.addElement("Wolfenstein");
最后就可以构造出一个X509Name对象:
X509Name SubjectDN = new X509Name(oids, attributes);
这样,我们的主体名称结构就确立起来了.
下面我们就可以讲关键的部分了,那就是如何用Java程序完成CA最重要的功能,签署证书.
要做CA,第一步要准备好自己的证书和私钥.私钥如何从文件里面读取出来前面已经讲过了.从文件系统中读出证书的代码如下:
CertificateFactory certCF = CertificateFactory.getInstance("X.509");
X509Certificate caCert = certCF.generateCertificate(certBIS);
这里cerBIS是一个InputStream类型的对象.例如一个标准的X509v3格式的证书文件所形成的输入流.
第二步就是从用户那里获取输入,然后构造主体名称结构DN,如何构造DN上次已经说过了,如何从用户那里获取输入,这个不在本文讨论范围之内.
下一步就是获取用户的公钥,好和他所需要的证书对应起来.也有不少CA的做法就是在这里替用户现场生成一对密钥对,然后把公钥放到证书中签发给用户.这个应该看实际需要选择合适的方式.
现在一切信息都已经准备好了,可以签发证书了,下面的代码说明了这个过程:
//构造一个证书生成器对象
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
// 从CA的证书中获取签发者的主体名称(DN)
// 这里有一点小技巧,我们要把JCE中定义的
// 用来表示DN的对象X500Principal转化成在
// BC Provider中的相应的对象X509Name
// 先从CA的证书中读取出CA的DN进行DER编码
DERInputStream dnStream =
new DERInputStream(
new ByteArrayInputStream(
caCert.getSubjectX500Principal().
getEncoded()));
// 马上又从编码后的字节流中读取DER编码对象
DERConstructedSequence dnSequence =
(DERConstructedSequence)dnStream.readObject();
// 利用读取出来的DER编码对象创建X509Name
// 对象,并设置为证书生成器中的"签发者DN"
certGen.setIssuerDN(new X509Name(dnSequence));
// 设置好证书生成器中的"接收方DN"
certGen.setSubjectDN(subjectDN);
// 设置好一些扩展字段,包括签发者和
// 接收者的公钥标识
certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false,
createSubjectKeyId(keyToCertify));
certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
createAuthorityKeyId(caCert.getPublicKey()));
// 设置证书的有效期和序列号
certGen.setNotBefore(startDate);
certGen.setNotAfter(endDate);
certGen.setSerialNumber(serialNumber);
// 设置签名算法,本例中使用MD5hash后RSA
// 签名,并且设置好主体的公钥
certGen.setSignatureAlgorithm("MD5withRSA");
certGen.setPublicKey(keyToCertify);
// 如果以上一切都正常地话,就可以生成证书了
X509Certificate cert = null;
cert = certGen.generateX509Certificate(caPrivateKey);
这里是上面用到的生成签发者公钥标识的函数:
protected AuthorityKeyIdentifier createAuthorityKeyId(PublicKey pubKey)
{
AuthorityKeyIdentifier authKeyId = null;
try
{
ByteArrayInputStream bIn = new ByteArrayInputStream(pubKey.getEncoded());
SubjectPublicKeyInfo info = new SubjectPublicKeyInfo(
(DERConstructedSequence)new DERInputStream(bIn).readObject());
authKeyId = new AuthorityKeyIdentifier(info);
}
catch (IOException e)
{
System.err.println("Error generating SubjectKeyIdentifier: " +
e.toString());
System.exit(1);
}
return authKeyId;
}
生成主体公钥标识的函数和上面的类似,把AuthorityKeyIdentifier替换成SubjectKeyIdentifier就可以了.
这里要注意的是,CA的公钥也是在一份证书里,这种证书的特点是签发者DN和接收者DN一样,也就是说,这种证书是CA自己给自己颁发的证书,也就是"自签名证书",它上面的公钥是CA自身的公钥,用来签名的私钥就是该公钥对应的私钥.一般每个CA都要有这么一份证书,除非该CA不是根CA,即它的权威性不是由它自己证明,而是由它的上级CA证明.但是,最后总归要有一个根CA,它为各个安全应用程序的用户所信赖.
到这里我们已经把CA最基本的功能如何用Java实现讲完了,下面讲如何从PKCS#10格式证书请求文件中读取出用户信息,然后直接签发公钥.
前面已经把如何用Java实现一个CA基本上讲完了.但是他们都有一个特点,就是用户的信息都是在现场获取的,不能做申请和签发相分离.现在我们要讲述的是PKCS#10证书请求文件.它的作用就是可以使申请和签发相分离.
PKCS#10证书请求结构中的主要信息包含了被签发者(证书申请者)的主体名称(DN)和他的公钥.因此一个CA在获取到一个PKCS#10证书请求后,就可以从中获取到任何和签发证书有关的信息,然后用它自己的私钥签发证书.
使用BC Provider在Java中构造一个证书请求格式的对象调用其构造函数即可,这个函数如下:
PKCS10CertificationRequest(java.lang.String signatureAlgorithm, X509Name subject, java.security.PublicKey key, ASN1Set attributes, java.security.PrivateKey signingKey)
它的参数是自签名算法,证书申请者的DN,证书申请者的公钥,额外的属性集(就是要申请的证书的扩展信息),申请证书者的私钥.申请证书者的私钥仅仅是用来进行一下自签名,并不出现在证书请求中,需要自签名的目的是保证该公钥确实为申请者所有.
调用该对象的getEncoded()方法可以将其进行DER编码,然后储存起来,该对象还有另一个构造函数:
PKCS10CertificationRequest(byte[] bytes)
这个构造函数的作用就是直接从储存的DER编码中把这个对象还原出来.
利用证书请求结构进行证书签发的代码如下,这里假设CSR是一个已经获取出来的PKCS10CertificationRequest结构:
PublicKey SubjectPublicKey = CSR.getPublicKey();
CertificationRequestInfo CSRInfo = CSR.getCertificationRequestInfo();
X509Name SubjectDN = CSRInfo.getSubject();
ASN1Set Attributes = CSRInfo.getAttributes();
这样,申请者的主体DN,申请者的公钥,申请者希望在证书扩展信息中填写的属性都得到了,剩下的事情就和用户在现场输入时一样了,其它的信息一般是申请者不能决定的.另外证书请求格式中有一样信息没有明确给出来,那就是证书的有效期,这个应该单独询问用户,或者用其它的方法保存起来.
ezerg 编程小语
本文地址:http://com.8s8s.com/it/it14242.htm