作者:肖波

背景

服务器SSL数字证书和客户端单位数字证书的格式遵循 X.509 标准。 X.509 是由国际电信联盟(ITU-T)制定的数字证书标准。为了提供公用网络用户目录信息服务, ITU 1988 年制定了 X.500 系列标准。其中 X.500 X.509 是安全认证系统的核心, X.500 定义了一种区别命名规则,以命名树来确保用户名称的唯一性; X.509 则为 X.500 用户名称提供了通信实体鉴别机制,并规定了实体鉴别过程中广泛适用的证书语法和数据接口, X.509 称之为证书。

   X.509 给出的鉴别框架是一种基于公开密钥体制的鉴别业务密钥管理。一个用户有两把密钥:一把是用户的专用密钥(简称为:私钥),另一把是其他用户都可得到和利用的公共密钥(简称为:公钥)。用户可用常规加密算法(如 DES)为信息加密,然后再用接收者的公共密钥对 DES 进行加密并将之附于信息之上,这样接收者可用对应的专用密钥打开 DES 密锁,并对信息解密。该鉴别框架允许用户将其公开密钥存放在CA的目录项中。一个用户如果想与另一个用户交换秘密信息,就可以直接从对方的目录项中获得相应的公开密钥,用于各种安全服务。

   最初的 X.509 版本公布于 1988 年,版本 3 的建议稿 1994 年公布,在 1995 年获得批准。本质上, X.509 证书由用户公共密钥与用户标识符组成,此外还包括版本号、证书序列号、CA 标识符、签名算法标识、签发者名称、证书有效期等。用户可通过安全可靠的方式向 CA 提供其公共密钥以获得证书,这样用户就可公开其证书,而任何需要此用户的公共密钥者都能得到此证书,并通过 CA 检验密钥是否正确。这一标准的最新版本 -- X.509 版本 3 是针对包含扩展信息的数字证书,提供一个扩展字段,以提供更多的灵活性及特殊环境下所需的信息传送。

   为了进行身份认证, X.509 标准及公共密钥加密系统提供了一个称作数字签名的方案。用户可生成一段信息及其摘要(亦称作信息指纹)。用户用专用密钥对摘要加密以形成签名,接收者用发送者的公共密钥对签名解密,并将之与收到的信息指纹进行比较,以确定其真实性。

   目前, X.509 标准已在编排公共密钥格式方面被广泛接受,已用于许多网络安全应用程序,其中包括 IP 安全( Ipsec )、安全套接层( SSL )、安全电子交易( SET )、安全多媒体 INTERNET 邮件扩展( S/MIME )等。

 

创建X509 证书

创建X509证书方法较多,在Windows 环境下大致总结了几中办法,

1)      通过CA获取证书,

2)      通过微软提供的makecert 工具得到测试证书

3)      编程的方法创建,.Net提供了 X509Certificate2 类,该类可以用于创建证书,但只能从RawData中创建,创建后无法修改除FriendlyName以外的任何属性。

 

我在互联网上找了很久,始终没有找到完全通过程序创建自定义的证书的方法(-_-!同感)。后来想了一个折中办法,就是用程序调用 makecert.exe 先生成一个证书,证书的一些参数如Subject,有效期,序列号等可以通过参数传入,然后把生成的证书文件读到Rawdata中,得到X509Certificate2 类型的证书对象。当然这种方法确实比较笨,必须要依赖外部进程。等后面有时间的话,我还是想按照X509 V3 标准,自己创建RawData,然后生成证书,这样应该是比较灵活的做法。不知道网友们有没有什么更好的方法来创建一个自定义的证书。

 

通过 makecert.exe 创建X509证书的代码如下,供大家参考

 

static object semObj = new object();

 

/// <summary>

/// 自定义的证书信息

/// </summary>

public class T_CertInfo

{

    public String FriendlyName;

    public String Subject;

    public DateTime BeginDate;

    public DateTime EndDate;

    public int SerialNumber;

}

 

/// <summary>

/// 生成X509证书

/// </summary>

/// <param name="makecrtPath">makecert进程的目录</param>

/// <param name="crtPath">证书文件临时目录</param>

/// <param name="certInfo">证书信息</param>

/// <returns></returns>

public static X509Certificate2 CreateCertificate(String makecrtPath, String crtPath,

    T_CertInfo certInfo)

{

    Debug.Assert(certInfo != null);

    Debug.Assert(certInfo.Subject != null);

 

    string MakeCert = makecrtPath + "makecert.exe";

    string fileName = crtPath + "cer";

 

    string userName = Guid.NewGuid().ToString();

 

    StringBuilder arguments = new StringBuilder();

 

    arguments.AppendFormat("-r -n \"{0}\" -ss my -sr currentuser -sky exchange ",

        certInfo.Subject);

 

    if (certInfo.SerialNumber > 0)

    {

        arguments.AppendFormat("-# {0} ", certInfo.SerialNumber);

    }

 

    arguments.AppendFormat("-b {0} ", certInfo.BeginDate.ToString(@"MM\/dd\/yyyy"));

    arguments.AppendFormat("-e {0} ", certInfo.EndDate.ToString(@"MM\/dd\/yyyy"));

    arguments.AppendFormat("\"{0}\"", fileName);

 

    lock (semObj)

    {

        Process p = Process.Start(MakeCert, arguments.ToString());

        p.WaitForExit();

 

        byte[] certBytes = ReadFile(fileName);

        X509Certificate2 cert = new X509Certificate2(certBytes);

        cert = new X509Certificate2(certBytes);

 

        if (certInfo.FriendlyName != null)

        {

            cert.FriendlyName = certInfo.FriendlyName;

        }

 

        return cert;

    }

}

 

 

internal static byte[] ReadFile(string fileName)

{

    using (FileStream f = new FileStream(fileName, FileMode.Open, FileAccess.Read))

    {

        int size = (int)f.Length;

        byte[] data = new byte[size];

        size = f.Read(data, 0, size);

        return data;

    }

}

 

获取证书私钥

通过上述方法得到的X509证书,只能获取其公钥信息,由于公钥私钥是成对出现的,如果我们要在程序中使用该证书来加解密,就必须要获取公钥对应的那个私钥。一样是在互联网上没有找到很好的解决办法,只能自己研究。目前总结出两种方法,给大家分享:

第一种方法:

从密钥容器中获取私钥。具体方法如下:

首先在 makecert 的参数中要加入一条 -sk keyname  指定主题的密钥容器位置,该位置包含私钥。如果密钥容器不存在,系统将创建一个。

然后 在执行完 p.WaitForExit(); 这一句后执行下面语句获取私钥和私钥参数

RSAParameters privateKey;

RSACryptoServiceProvider rsa = GetKeyFromContainer("keyname");

privateKey = rsa.ExportParameters(true);

 

public static RSACryptoServiceProvider GetKeyFromContainer(string ContainerName)

{

    // Create the CspParameters object and set the key container

    // name used to store the RSA key pair.

    CspParameters cp = new CspParameters();

    cp.KeyContainerName = ContainerName;

 

    // Create a new instance of RSACryptoServiceProvider that accesses

    // the key container MyKeyContainerName.

    return new RSACryptoServiceProvider(cp);

}

 

这种方法有一个缺点就是程序的调用者必须要具备读取密钥容器的权限才行,如果是Web应用,由于IIS来宾帐户没有这个权限,将无法读取密钥容器中的密钥。尝试采用模拟超级用户登录的方法NetworkSecurity.ImpersonateUser),也无法解决这个问题,而且这样做我个人觉得对网站的安全性方面也不是很好。后来想出了第二种方法,就是干脆重置密钥对,用自己生成的密钥对替换证书中的密钥对,试了一下,还是行之有效的。

第二种方法:

重置密钥对,方法如下:

首先要生成一个加密算法和加密位数与makecert生成的证书密钥相同的密钥。通过实测发现makecert采用交换密钥时,默认产生一个1024RSA密钥,Exponent 1,0,1,这和

RSACryptoServiceProvider 默认的密钥是相同的。所以只要用 RSACryptoServiceProvider RSA = new RSACryptoServiceProvider() 生成一个密钥就可以了。

 

第二步就是替换,也就是将密钥文件中公钥参数替换为要置换的公钥参数。

 

RSAParameters publicKey;

RSAParameters privateKey;

 

RSACryptoServiceProvider RSA = (RSACryptoServiceProvider)cert.PublicKey.Key;

publicKey = RSA.ExportParameters(false);

 

//查找公钥参数在RawData中的位置

if (publicKey.Modulus.Length != 128 || publicKey.Exponent.Length != 3)

{

    throw new Exception("public key module lenght != 128!");

}

 

if (publicKey.Exponent[0] != 1 ||

    publicKey.Exponent[1] != 0 ||

    publicKey.Exponent[2] != 1)

{

    throw new Exception("public key Exponent != 101!");

}

 

byte[] module = publicKey.Modulus;

 

int i = 0;

int matchCnt = 0;

int modulePos = 0;

int j = 0;

 

while (i < certBytes.Length)

{

 

    if (certBytes[i] == module[j]) //cerBytes RawData,什么可以参加创建证书的代码

    {

        i++;

        j++;

        matchCnt++;

 

        if (matchCnt == 128)

        {

            modulePos = i - 128;

            break;

        }

    }

    else

    {

        if (matchCnt == 128)

        {

            modulePos = i - 128;

            break;

        }

        else

        {

            matchCnt = 0;

            j = 0;

            i++;

        }

    }

}

 

//创建密钥对

RSA = new RSACryptoServiceProvider();

publicKey = RSA.ExportParameters(false);

privateKey = RSA.ExportParameters(true);

 

//将要重置的密钥对中的公钥参数覆盖原参数

 

j = 0;

for (i = modulePos; i < modulePos + 128; i++)

{

    certBytes[i] = publicKey.Modulus[j];

    j++;

}

 

//用新参数重新创建证书

cert = new X509Certificate2(certBytes);

 

这样一来privateKey就成了新证书的私钥了。

 

这种方法的问题:

这种方法的问题是查找证书中公钥信息,是通过匹配的方式来做的,这是一个偷懒的方法,正确的做法应该是按照标准的定义来查找,由于暂时没有太多时间去仔细研究标准,所以就偷了一个懒,但感觉这种方法目前来说还是行之有效的,待以后改进吧。