将 Java 公钥哈希移植到 C# .NET






4.62/5 (40投票s)
2004年4月7日
7分钟阅读

269646

4771
本教程旨在解决希望使用 Java 密钥库 (Java Key Store) 对数据进行签名并在 .NET 平台验证的开发人员所面临的问题。本教程演示了如何使用 Java 导出公钥,并将其转换为 .NET 兼容的公钥(XML 格式)。
目录
摘要
或许没有一个软件工程主题比数据安全和完整性更具时效性。攻击代价高昂,无论是来自内部还是外部的攻击,有些攻击可能导致软件公司承担损害赔偿责任。掌握最新的技术和工具是应用安全和完整性的关键之一。开发跨平台应用程序以确保安全是许多开发人员面临的一个大问题。
本教程旨在解决开发人员在不同平台之间签名数据并在另一个平台验证时遇到的此类问题,使用一种流行的公钥数字签名算法 RSA。
教程首先提供一个 Java 程序,说明如何生成密钥,使用生成的密钥对数据进行签名和验证,通过分解应用程序的功能和查看最终执行结果来达到这一目的。接下来,教程讨论如何导出 Java 生成的公钥,供 .NET 框架稍后在 .NET 应用程序中验证数据。最后,本教程讨论如何在 .NET(使用 C#)中读取 Java 程序生成的公钥,并使用此公钥验证数据。教程结尾还提供了不同术语的定义,以便为初学者提供帮助。
第一部分:引言
谁应该阅读?
本教程的核心思想围绕着在 Java 中对数据进行签名,然后使用 C# 在 .NET 中进行验证。但是,有兴趣开始学习使用 Java 加密 API 和语言进行数据签名和验证的读者也可以使用本教程。本教程假设读者了解如何阅读和编写基本的 Java 和 C# .NET 应用程序。
在阅读本教程之前,强烈建议您应具备 Java 和 .NET 框架中定义的加密和安全包术语的基础知识。对于感兴趣的读者,术语定义已在教程末尾的词汇表中提供。即使您熟悉这些术语,也请浏览一下以重新记忆。
第二部分:Java 中的签名生成
zip 文件包含 SecurityManager.java。这个 Java 类负责使用私钥和公钥分别对数据进行签名和验证。它还为 .NET 框架生成 XML 公钥。此类具有以下职责。
- 初始化签名参数
- 生成公钥和私钥
- 存储/读取生成的公钥和私钥
- 对输入数据进行签名
- 验证输入数据
- 以 .NET 兼容(XML)格式存储公钥
虽然将所有内容都放在一个类中不太合理;但为了保持简单,所有内容都放在了一个类中。读者可以修改代码,并将存储/读取 HDD 上的密钥等功能移至其他类。
代码已经进行了良好的注释,希望读者不会觉得难以理解。
公钥和私钥生成
数字签名生成必须使用私钥。生成私钥时,相应的公钥会自动生成。为了提高性能,私钥/公钥仅生成一次并存储在 HDD 的物理位置。当前代码采用的方法是将公钥存储在某个应用程序路径下。但是,对于 Web 应用程序,密钥可以与数据一起流式传输。请参阅 图。
以下函数生成公钥和私钥,并将它们存储在指定位置。此函数还调用函数来生成兼容的 .NET 公钥。它以哈希算法的大小作为参数,并生成相应的密钥。
现在,让我们看看相应的代码
/**
* Generates the keys for given size.
* @param size - Key Size [512|1024]
*/
public void generateKeys(int size) {
try {
System.out.println("Generating Keys");
keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(size);
keypair = keyGen.genKeyPair();
//Get the KeyPair for given algorithm and size.
privateKey = keypair.getPrivate(); //Extract Private Key
publicKey = keypair.getPublic(); // Extract Public Key
// Get bytes of the public and private keys
byte[] privateKeyBytes = privateKey.getEncoded(); //Get the key bytes
byte[] publicKeyBytes = publicKey.getEncoded(); // Get the key bytes
//write bytes to corresponding files after encoding
//them to Base 64 using Base64Encoder Class.
writeKeyBytesToFile(new
BASE64Encoder().encode(privateKeyBytes).getBytes(),
PRIVATE_KEY_FILE);
String encodedValue = new BASE64Encoder().encode(publicKeyBytes);
writeKeyBytesToFile(encodedValue.getBytes(), PUBLIC_KEY_FILE);
//Generate the Private Key, Public Key and Public Key in XML format.
PrivateKey privateKey =
KeyFactory.getInstance("RSA").generatePrivate(new
PKCS8EncodedKeySpec(privateKeyBytes));
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(new
X509EncodedKeySpec(publicKeyBytes));
//Create and Instance of RSAPublicKey class with X509 encoding
RSAPublicKey rsaPublicKey =
(RSAPublicKey)
KeyFactory.getInstance("RSA").generatePublic(new
X509EncodedKeySpec(publicKeyBytes));
//Get the RSAPublicKey as XML store the public key in XML string
//to make compatible .Net public key
//file. See getRSAPublicKeyAsXMLString() for details
String xml = getRSAPublicKeyAsXMLString(rsaPublicKey);
//Store the XML (Generated .Net public key file) in file
writeKeyBytesToFile(xml.getBytes(), DOT_NET_PUBLIC_KEY_FILE);
}
catch (java.security.NoSuchAlgorithmException e) {
System.out.println(
"No such algorithm. Please check the JDK version."+e.getCause());
}
catch (java.security.spec.InvalidKeySpecException ik) {
System.out.println(
"Invalid Key Specs. Not valid Key files."+ ik.getCause());
}
catch (UnsupportedEncodingException ex) {
System.out.println(ex);
}
catch (ParserConfigurationException ex) {
System.out.println(ex);
}
catch (TransformerException ex) {
System.out.println(ex);
}
catch (IOException ioe) {
System.out.println("Files not found on specified path. "+
ioe.getCause());
}
}
密钥生成后,它们可以存储在特定路径的简单文件中,以避免重新生成私钥和公钥。这可以提高性能。可能需要为每个请求生成新的密钥。如果您想每次都生成密钥,请在 main 函数或应用程序中签名数据之前每次调用此函数。
从存储文件中初始化公钥和私钥
以下两个函数读取公钥和私钥。无论何时您需要对数据进行签名,都只需要私钥。因此,我提供了一个额外的函数来仅初始化私钥。此函数的基本工作流程在 图 中描述。
现在让我们看看代码。顾名思义,initializePrivateKey
只初始化私钥,但如果您想同时初始化私钥和公钥,请调用名为 initializeKeys
的另一个函数。
/**
* Initialize only the private key.
*/
private void initializePrivateKey() {
try {
//Read key files back and decode them from BASE64
BASE64Decoder decoder = new BASE64Decoder();
byte[] privateKeyBytes = decoder.decodeBuffer(new
String(readPrivateKeyFile()));
// Convert back to public and private key objects
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
EncodedKeySpec privateKeySpec = new
PKCS8EncodedKeySpec(privateKeyBytes);
privateKey = keyFactory.generatePrivate(privateKeySpec);
}
catch (IOException io) {
System.out.println("Private Key File Not found."+
io.getCause());
}
catch (InvalidKeySpecException e) {
System.out.println("Invalid Key Specs. Not valid Key files."
+ e.getCause());
}
catch (NoSuchAlgorithmException e) {
System.out.println("There is no such algorithm." +
" Please check the JDK ver."+ e.getCause());
}
}
/**
* Initializes the public and private keys.
*/
private void initializeKeys() {
try {
//Read key files back and decode them from BASE64
BASE64Decoder decoder = new BASE64Decoder();
byte[] privateKeyBytes = decoder.decodeBuffer(new
String(readPrivateKeyFile()));
byte[] publicKeyBytes = decoder.decodeBuffer(new
String(readPublicKeyFile()));
// Convert back to public and private key objects
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
EncodedKeySpec privateKeySpec =
new PKCS8EncodedKeySpec(privateKeyBytes);
privateKey = keyFactory.generatePrivate(privateKeySpec);
EncodedKeySpec publicKeySpec =
new X509EncodedKeySpec(publicKeyBytes);
publicKey = keyFactory.generatePublic(publicKeySpec);
}
catch (IOException io) {
System.out.println("Public/ Private Key File Not found."
+ io.getCause());
}
catch (InvalidKeySpecException e) {
System.out.println("Invalid Key Specs. Not valid Key files."
+ e.getCause());
}
catch (NoSuchAlgorithmException e) {
System.out.println("There is no such algorithm." +
" Please check the JDK ver."+ e.getCause());
}
}
数据签名
从函数名称可以明显看出,该函数负责对作为字节发送的数据进行签名。在此函数调用之前,必须初始化私钥。要么读取现有私钥,要么生成新的私钥。请参阅 图。
/**
* Signs the data and return the signature for a given data.
* @param toBeSigned Data to be signed
* @return byte[] Signature
*/
public byte[] signData(byte[] toBeSigned) {
if (privateKey == null) {
initializePrivateKey();
}
try {
Signature rsa = Signature.getInstance("SHA1withRSA");
rsa.initSign(privateKey); //initialize the signature with provided key
rsa.update(toBeSigned); //sign the data
return rsa.sign(); //return the signature in bytes
}
catch (NoSuchAlgorithmException ex) {
System.out.println(ex);
}
catch (InvalidKeyException in) {
System.out.println("Invalid Key file." +
"Please chekc the key file path"+ in.getCause());
}
catch (SignatureException se) {
System.out.println(se);
}
return null;
}
数据和签名验证
数据验证非常简单。以下函数负责验证作为字节发送的相应数据的签名。使用公钥验证签名。请参阅 图。
/**
* Verifies the signature for the given bytes using the public key.
* @param signature Signature
* @param data Data that was signed
* @return boolean True if valid signature else false
*/
public boolean verifySignature(byte[] signature, byte[] data) {
try {
// Initialize the keys before verifying the signature
initializeKeys();
//Initialize the signature engine instance with public key
sign.initVerify(publicKey);
// load the data to be verified
sign.update(data);
// verify the data loaded in previous step
// with given signature and return the
return sign.verify(signature);
//result
}
catch (SignatureException e) {
e.printStackTrace();
}
catch (InvalidKeyException e) {
}
return false;
}
第三部分:生成 .NET 兼容的公钥
.NET RSA 公钥 XML
.NET RSA 公钥包含模数 (Modulus) 和指数 (Exponent),可以从 Java 公钥中提取。在我们深入研究 Java 代码细节之前,让我们先看看可以转换为 RSAParameters
结构体的 XML。Modulus
代表 RSA 算法的模数参数,而 Exponent
代表 RSA 算法的指数参数。 .NET 中的 RSA 算法类位于 System.Security.Cryptography.RSA
命名空间下。
.NET RSA 公钥 XML 结构如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<RSAKeyValue>
<Modulus>uKI+0wG6eILUPRNf6ImqRdez/nLxsV0LHGsuvYR0LDVrXz8Y7sYSlpAkn1HpJI8US8Sx5bJzvBib
vKv0pAa7UQ==
</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
从 Java 生成 .NET XML 公钥
在本节中,我们将讨论导出 Java 公钥类为 .NET 兼容公钥所采用的方法。导出的 XML 密钥稍后将在我们的 .NET 应用程序中加载,我们将使用此密钥验证签名。
让我们看看代码。
回想我们在密钥生成过程中调用了一些函数来生成 XML 格式的公钥。
//Create and
Instance of RSAPublicKey class with X509 encoding key specs
RSAPublicKey rsaPublicKey = (RSAPublicKey)
KeyFactory.getInstance("RSA").
generatePublic(new X509EncodedKeySpec(publicKeyBytes));
//Get the RSAPublicKey as XML store the public key
//in XML string to make compatible .Net public key
//file. See getRSAPublicKeyAsXMLString() for details
String xml = getRSAPublicKeyAsXMLString(rsaPublicKey);
//Store the XML (Generated .Net public key file) in file
writeKeyBytesToFile(xml.getBytes(), DOT_NET_PUBLIC_KEY_FILE);
如果看到第一行,使用了 X509EncodedKeySpec
。此类表示公钥的 ASN.1 编码,根据 ASN.1 类型 SubjectPublicKeyInfo
进行编码。SubjectPublicKeyInfo
语法在 X.509 标准中定义如下
SubjectPublicKeyInfo ::= SEQUENCE {
algorithm AlgorithmIdentifier,
subjectPublicKey BIT STRING }
现在,看看名为 getRSAPublicKeyAsXMLString
的函数
/**
* Gets the RSA Public key as XML string.
* @param key RSAPublicKey
* @return String XML representation of RSA Public Key.
* @throws UnsupportedEncodingException
* @throws ParserConfigurationException
* @throws TransformerException
*/
private String getRSAPublicKeyAsXMLString(RSAPublicKey key) throws
UnsupportedEncodingException,
ParserConfigurationException,
TransformerException {
Document xml = getRSAPublicKeyAsXML(key);
Transformer transformer =
TransformerFactory.newInstance().newTransformer();
StringWriter sw = new StringWriter();
transformer.transform(new DOMSource(xml), new StreamResult(sw));
return sw.getBuffer().toString();
}
/**
* Gets the RSA Public Key as XML. The idea is to make the key readable
for
* .Net platform. The generated key is compatible with .Net key structure.
* @param key RSAPublicKey
* @return Document XML document.
* @throws ParserConfigurationException
* @throws UnsupportedEncodingException
*/
private Document getRSAPublicKeyAsXML(RSAPublicKey key) throws
ParserConfigurationException,
UnsupportedEncodingException {
Document result =
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element rsaKeyValue = result.createElement("RSAKeyValue");
result.appendChild(rsaKeyValue);
Element modulus = result.createElement("Modulus");
rsaKeyValue.appendChild(modulus);
byte[] modulusBytes = key.getModulus().toByteArray();
modulusBytes = stripLeadingZeros(modulusBytes);
// KeyManager.write("c:\\mod.c",
// new sun.misc.BASE64Encoder().encode(modulusBytes));
// //Stored it for testing purposes
modulus.appendChild(result.createTextNode(
new String(new sun.misc.BASE64Encoder().encode(modulusBytes))));
Element exponent = result.createElement("Exponent");
rsaKeyValue.appendChild(exponent);
byte[] exponentBytes = key.getPublicExponent().toByteArray();
// KeyManager.write("C:\\exponenet.c",
// new sun.misc.BASE64Encoder().encode(exponentBytes));
// //stored it for testing purposes
exponent.appendChild(result.createTextNode(
new String(new sun.misc.BASE64Encoder().encode(exponentBytes))));
return result;
}
上述函数允许我们生成包含公钥信息的 XML 文件。我们将此文件存储在硬盘上或通过网络传输。
第四部分:在 .NET 中读取公钥和验证数据
在本节中,我们将读取上一节中 Java 程序生成的公钥。
初始化
在构造函数中,初始化 RSAParameters
类和 RSACryptoServiceProvider
。
PUBLIC_KEY="c:\\netpublic.key"; //Generated by Java Program
RSAKeyInfo = new RSAParameters();
RSA = new RSACryptoServiceProvider();
在 .NET 中读取公钥
以下函数从 HDD 上的特定路径读取文件。可以使用网络流读取 XML 文件。解析模数数据和指数数据,并将它们赋给变量。将 Base64 字符串转换为字节数组,并将其赋给 RSAParameters.Modulus
和 RSAParameters.Exponent
。RSACryptoServiceProvider
的 ImportParameters
函数用于加载 RSA 算法的参数。
private void readKey()
{
// read the XML formated public key
try
{
XmlTextReader reader = new XmlTextReader(PUBLIC_KEY);
while(reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if(reader.Name=="Modulus")
{
reader.Read();
modStr= reader.Value;
}
else if(reader.Name=="Exponent")
{
reader.Read();
expStr= reader.Value;
}
}
}
if(modStr.Equals("") ||expStr.Equals(""))
{
//throw exception
throw new Exception("Invalid public key");
}
RSAKeyInfo.Modulus = Convert.FromBase64String(modStr);
RSAKeyInfo.Exponent = Convert.FromBase64String(expStr);
RSA.ImportParameters(RSAKeyInfo);
}
catch(Exception e)
{
throw new Exception("Invalid Public Key.");
}
}
在 .NET 中验证签名
一旦模数和指数从 XML 解析并加载到 RSACryptoServiceProvider
中,就可以验证签名。调用 verifySignature
方法来验证给定数据的签名。它通过将指定签名数据与为指定数据计算的签名进行比较来验证签名。请参阅 图。
public bool verifySignature(byte[]
signature , string signedData)
{
byte[] hash = Convert.FromBase64String(signedData);
try
{
if(RSA.VerifyData(hash,"SHA1",signature))
{
//Console.WriteLine("The signature is valid.");
return true;
}
else
{
//Console.WriteLine("The signature is not valid.");
return false;
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
return false;
}
}
参考文献
致谢
特别感谢:
- Dr. Iman Gholampur
- Philip Ross
- Mitch Gallant