背景
DSA(Digital signature algorithm)是一个非对称加密的签名算法。使用私钥对明文的摘要进行加密,使用配对的公钥对密文进行验证。算法的介绍,甚至是程序demo在网上可以找到很多,但绝大多数都是在同一个平台中进行签名和验证(如C++、.NET或java),当进行跨平台的验证时却发现问题并不简单。
开始时使用openssl在C++下验证java发过来的一个签名,尝试过各种方案最终以失败告终。转而使用crypto++(http://www.cryptopp.com/),我使用的版本是5.6.2。
在Codeproject上找到一篇Jeffrey Walton的文章(Cryptographic Interoperability: Digital Signatures),里面介绍了java签名和c++签名格式其实是不一样的,java用DER格式,而C++用IEEE的P1363格式,在验证时需要转换相应的格式。作者提供了在.Net、C++、Java三个平台上的签名及验证的Demo。
关键代码逻辑贴出来大家共同学习一下,一下三段代码分别实现了公私密钥的生成、签名、验证签名。完整的项目包可在原文中下载,或者点击下面每个章节的链接:
DSA签名之C++(Crypto++)
// CryptoPP.cpp
//
#include "stdafx.h"
#include <dsa.h>
using CryptoPP::DSA;
using CryptoPP::DSA_DER;
using CryptoPP::DSA_P1363;
#include <pubkey.h>
using CryptoPP::PrivateKey;
using CryptoPP::PublicKey;
#include <osrng.h>
using CryptoPP::AutoSeededRandomPool;
#include <files.h>
using CryptoPP::FileSource;
using CryptoPP::FileSink;
using CryptoPP::StringSource;
using CryptoPP::StringSink;
bool CreateDSAKeys();
bool SignDSAMessage();
bool VerifyDSAMessage();
int main(int argc, char* argv[])
{
CreateDSAKeys();
SignDSAMessage();
VerifyDSAMessage();
return 0;
}
bool CreateDSAKeys()
{
AutoSeededRandomPool prng;
try
{
// Crypto++ Key Generation
DSA::Signer signer;
PrivateKey& privateKey = signer.AccessPrivateKey();
privateKey.GenerateRandom( prng );
DSA::Verifier verifier( signer );
PublicKey& publicKey = verifier.AccessPublicKey();
// Crypto++ Save Keys
privateKey.Save(FileSink("private.dsa.cpp.key"));
publicKey.Save(FileSink("public.dsa.cpp.key"));
}
catch( CryptoPP::Exception& ex )
{
cerr << "Caught Error:" << endl;
cerr << " " << ex.what() << endl;
return false;
}
catch( std::exception& ex )
{
cerr << "Caught Error:" << endl;
cerr << " " << ex.what() << endl;
return false;
}
return true;
}
bool VerifyDSAMessage()
{
try
{
//
// Crypto++ Load the Pubic Key
//
DSA::Verifier verifier;
PublicKey& publicKey = verifier.AccessPublicKey();
// publicKey.Load(FileSource("public.dsa.cpp.key", true));
// publicKey.Load(FileSource("public.dsa.java.key", true));
publicKey.Load(FileSource("public.dsa.cs.key", true));
//
// Retrieve the Message and the Signature on Message
//
string message, signature;
// C++
FileSource( "dsa.cpp.msg", true, new StringSink( message ) );
FileSource( "dsa.cpp.sig", true, new StringSink( signature ) );
// Java
// FileSource( "dsa.java.msg", true, new StringSink( message ) );
// FileSource( "dsa.java.sig", true, new StringSink( signature ) );
// C#
// FileSource( "dsa.cs.msg", true, new StringSink( message ) );
// FileSource( "dsa.cs.sig", true, new StringSink( signature ) );
//*
// --- Begin Java Specific Conversion ---
//
// Convert: DER -> P1363
//
// Java is DER Encoded Sequence.
// Crypto++ is P1363 format.
/*
{
// From dsa.h: If toFormat == DSA_P1363,
// bufferSize must equal publicKey.SignatureLength()
size_t length = verifier.SignatureLength();
// A buffer for the conversion
byte* buffer = new byte[ length ];
// Shit or Go Blind. You make the choice.
if( !buffer ) { return false; }
// Reuse length
length = CryptoPP::DSAConvertSignatureFormat(
buffer, length, DSA_P1363, (const byte*)signature.c_str(),
signature.length(), DSA_DER );
// Reinitialize signature so that it can be used
// in the verifier below with minimal effort
signature = string( (const char*)buffer, length );
delete[] buffer;
}
*/
// --- End Java Specific Conversion ---
//*/
//
// Verify the Signature on the Message
//
bool result = verifier.VerifyMessage(
(const byte*)message.c_str(), message.length(),
(const byte*)signature.c_str(), signature.length() );
if( result )
{
cout << "Signature on Message verified" << endl;
}
else
{
cerr << "Signature on Message not verified" << endl;
return false;
}
//
// Convert the Message
//
int nChars = MultiByteToWideChar( CP_UTF8, 0,
message.c_str(), -1, NULL, NULL );
wchar_t* p = new wchar_t[ nChars ];
if( !p ) { return false; }
MultiByteToWideChar( CP_UTF8, 0, message.c_str(), -1, p, nChars );
wstring wide( p );
delete[] p;
}
catch( CryptoPP::Exception& ex )
{
cerr << "Caught Error:" << endl;
cerr << " " << ex.what() << endl;
return false;
}
catch( std::exception& ex )
{
cerr << "Caught Error:" << endl;
cerr << " " << ex.what() << endl;
return false;
}
return true;
}
bool SignDSAMessage()
{
try
{
//
// Crypto++ Load Private Key
//
DSA::Signer signer;
PrivateKey& privateKey = signer.AccessPrivateKey();
privateKey.Load(FileSource("private.dsa.cpp.key", true));
// Convert the Message
wstring wide = L"Crypto Interop: \u9aa8";
int nChars = WideCharToMultiByte( CP_UTF8, 0,
wide.c_str(), -1, NULL, 0, NULL, FALSE );
char* p = new char[ nChars ];
if( !p ) { return false; }
nChars = WideCharToMultiByte( CP_UTF8, 0,
wide.c_str(), -1, p, nChars, NULL, FALSE );
string narrow( p );
delete[] p;
//
// Sign the Message
//
// Set up for SignMessage()
byte* s = new byte[ signer.MaxSignatureLength() ];
if( !s ) { return false; }
// Sign...
AutoSeededRandomPool prng;
size_t length = signer.SignMessage( prng,
(const byte*) narrow.c_str(), narrow.length(), s );
// Convenience
string signature( (const char*)s, length );
// --- Begin Java Specific Conversion ---
//
// Convert: P1363 -> DER
//
// Crypto++ uses P1363 format.
// Java requires a DER encoded sequence.
/*
{
// Determine size of required buffer
// We can call with a null buffer and 0 size
// when going in this direction
length = CryptoPP::DSAConvertSignatureFormat(
NULL, 0, DSA_DER, (const byte*)signature.c_str(),
signature.length(), DSA_P1363 );
// A buffer for the conversion
byte* buffer = new byte[ length ];
// Shit or Go Blind. You make the choice.
if( !buffer ) { return false; }
// Reuse length
length = CryptoPP::DSAConvertSignatureFormat(
buffer, length, DSA_DER, (const byte*)signature.c_str(),
signature.length(), DSA_P1363 );
// Reinitialize signature so that it can be used
// in the verifier below with minimal effort
signature = string( (const char*)buffer, length );
delete[] buffer;
}
*/
// --- End Java Specific Conversion ---
//
// Save the Message and the Signature on Message
//
// mofs: message filestream
// sofs: signature filestream
ofstream mofs, sofs;
mofs.open("dsa.cpp.msg", ios_base::binary | ios_base::trunc );
sofs.open("dsa.cpp.sig", ios_base::binary | ios_base::trunc );
// Save Original Message
mofs.write( narrow.c_str(), (int)narrow.length() );
// Save Signature on Message
sofs.write( (const char*)signature.c_str(), (int)length );
// Cleanup
sofs.close();
mofs.close();
}
catch( CryptoPP::Exception& ex )
{
cerr << "Caught Error:" << endl;
cerr << " " << ex.what() << endl;
return false;
}
catch( std::exception& ex )
{
cerr << "Caught Error:" << endl;
cerr << " " << ex.what() << endl;
return false;
}
return true;
}
在Linux环境上编译crypto++的时候比较顺利,而在AIX一个比较老的平台上编译的时候各种报错:
"pubkey.h", line 1606.39: 1540-0716 (S) The template argument "CryptoPP::TF_ES" does not match the template parameter "class ALG_INFO".
升级了xlC,并将GNUmakefile修改后终于编译成功:
CXXFLAGS = -DNDEBUG -q64 -qkeepinlines -brtl -bhalt:5
# -O3 fails to link on Cygwin GCC version 4.5.3
# -fPIC is supported. Please report any breakage of -fPIC as a bug.
# CXXFLAGS += -fPIC
# the following options reduce code size, but breaks link or makes link very slow on some systems
# CXXFLAGS += -ffunction-sections -fdata-sections
LDFLAGS = -q64 -bnoentry -qmkshrobj -lpthread
CXX = xlC
# 省略。。。
libcryptopp.so: $(LIBOBJS)
$(CXX) -o $@ $(LIBOBJS) $(LDFLAGS)
此外,makefile中用了wildcard作为cpp源代码列表。曾不小心把测试程序也放在同一个目录下,导致编出来的libcryptopp.so中包含一个main入口,再次编测试程序的时候就报冲突了。这里也吐槽一下crypto++的目录层级(只有一级)比openssl的目录层级差太多了。
在编译测试程序时上面的C++代码加载key文件的地方仍报错,无法将FileSource转换为CryptoPP::BufferedTransformation。从Crypto++的代码来看FileSource是BufferedTransformation的子类,因此改成下面的写法:FileSource *fs = new FileSource(_priKeyPath, true);
privateKey.Load((CryptoPP::BufferedTransformation&)*fs);
delete fs;
而在vs2013升级后的项目直接编译运行时总是报错,无奈注释掉了cryptlib.h中1091行的一个校验,不影响功能。
voidDoQuickSanityCheck()const {/*ThrowIfInvalid(NullRNG(),0);*/}
此外还遇到了内存泄露问题,最终发现是MTd/MT/MDd/MD选项的设置问题,这个可以搜到很多介绍的文章。
DSA签名之Java
package JavaInteropSign;
import java.io.*;
import java.security.*;
import java.security.spec.*;
import javax.swing.JOptionPane;
/**
* @author jeffrey walton
**/
public class Main {
static String ALGORITHM = "DSA";
public static void main(String[] args) {
try {
CreateDSAKeys();
SignDSAMessage();
VerifyDSAMessage();
} catch (Exception e) {
System.err.println("Main: Exception " + e.toString());
}
}
private static void VerifyDSAMessage() {
try {
// Load the public
PublicKey publicKey = LoadPublicKey("public.dsa.java.key");
// Load the message from file
FileInputStream mis = new FileInputStream("dsa.java.msg");
byte[] message = new byte[mis.available()];
mis.read(message); mis.close();
// Display the resurrected string
JOptionPane.showMessageDialog(null,
new String(message, 0, message.length, "UTF-8"));
// Load the signature of the message from file
FileInputStream sis = new FileInputStream("dsa.java.sig");
byte[] signature = new byte[sis.available()];
sis.read(signature); sis.close();
// Initialize Signature Object
Signature verifier = Signature.getInstance(ALGORITHM);
verifier.initVerify(publicKey);
// Load the message into the Verifier Object
verifier.update(message);
// Verify the Signature on the Message
boolean result = verifier.verify(signature);
StringBuilder sb = new StringBuilder();
if( result )
{
sb.append("Message Verified:\n");
sb.append(new String(message, 0, message.length, "UTF-8"));
}
else
{
sb.append("Message Not Verified");
}
JOptionPane.showMessageDialog(null, sb.toString());
} catch (Exception e) {
System.err.println("VerifyDSAMessage: " + e.toString());
}
}
private static void SignDSAMessage() {
try {
// Retrieve the Private Key
PrivateKey privateKey = LoadPrivateKey("private.dsa.java.key");
// Create the signer object
Signature signer = Signature.getInstance(ALGORITHM);
signer.initSign(privateKey, new SecureRandom());
// Prepare the Message
String s = "Crypto Interop: \u9aa8";
// Save the binary of the String which we will sign
byte[] message = s.getBytes("UTF-8");
// Insert the message into the signer object
signer.update(message);
byte[] signature = signer.sign();
// mos: message filestream
// sos: signature filestream
FileOutputStream mos = new FileOutputStream("dsa.java.msg");
mos.write(message);
FileOutputStream sos = new FileOutputStream("dsa.java.sig");
sos.write(signature);
} catch (Exception e) {
System.err.println("SignDSAMessage: " + e.toString());
}
}
private static PublicKey LoadPublicKey(String filename) {
PublicKey key = null;
try {
FileInputStream fis = new FileInputStream(filename);
byte[] b = new byte[fis.available()];
fis.read(b);
fis.close();
X509EncodedKeySpec spec = new X509EncodedKeySpec(b);
KeyFactory factory = KeyFactory.getInstance(ALGORITHM);
key = factory.generatePublic(spec);
} catch (Exception e) {
System.err.println("LoadPublicKey: " + e.toString());
}
return key;
}
private static PrivateKey LoadPrivateKey( String filename) {
PrivateKey key = null;
try {
FileInputStream fis = new FileInputStream(filename);
byte[] b = new byte[fis.available()];
fis.read(b);
fis.close();
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(b);
KeyFactory factory = KeyFactory.getInstance("DSA");
key = factory.generatePrivate(spec);
} catch (Exception e) {
System.err.println("LoadPrivateKey: " + e.toString());
}
return key;
}
private static void CreateDSAKeys() throws NoSuchAlgorithmException {
// http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html
// KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA", "SUN");
KeyPairGenerator kpg = KeyPairGenerator.getInstance(ALGORITHM);
kpg.initialize(1024, new SecureRandom());
KeyPair keys = kpg.generateKeyPair();
PrivateKey privateKey = keys.getPrivate();
PublicKey publicKey = keys.getPublic();
// Serialize Keys
SaveKey("private.dsa.java.key", privateKey);
SaveKey("public.dsa.java.key", publicKey);
}
static void SaveKey(String filename, Key key) {
try {
if (null == key) {
throw new Exception("key is null.");
}
FileOutputStream fos = new FileOutputStream(filename);
// PKCS #8 for Private, X.509 for Public
// File will contain OID 1.2.840.10040.4.1 (DSA)
// http://java.sun.com/j2se/1.4.2/docs/api/java/security/Key.html
fos.write(key.getEncoded());
fos.close();
} catch (Exception e) {
System.err.println("SaveEncodedKey: Exception " + e.toString());
}
}
}
其实java的实现逻辑比较通用,在网上能搜到大量的dsa签名和验证的文章都和这段代码类似:PKCS8私钥加密,X509公钥验证。
DSA签名之C#
using System;
using System.IO;
using System.Text;
using System.Windows.Forms;
using System.Security.Cryptography;
namespace CSInteropSign
{
class Program
{
internal static void Main(string[] args)
{
CreateDsaKeys();
SignDsaMessage();
VerifyDsaMessage();
}
private static void VerifyDsaMessage()
{
//
// Load the Public Key
// X.509 Format
//
AsnKeyParser keyParser =
new AsnKeyParser("public.dsa.cs.key");
DSAParameters publicKey = keyParser.ParseDSAPublicKey();
//
// Initailize the CSP
//
CspParameters csp = new CspParameters();
// Cannot use PROV_DSS_DH
const int PROV_DSS = 3;
csp.ProviderType = PROV_DSS;
const int AT_SIGNATURE = 2;
csp.KeyNumber = AT_SIGNATURE;
csp.KeyContainerName = "DSA Test (OK to Delete)";
//
// Initialize the Provider
//
DSACryptoServiceProvider dsa =
new DSACryptoServiceProvider(csp);
dsa.PersistKeyInCsp = false;
//
// The moment of truth...
//
dsa.ImportParameters(publicKey);
//
// Load the message
// Message is m
//
byte[] message = null;
using (BinaryReader reader = new BinaryReader(
new FileStream("dsa.cs.msg", FileMode.Open, FileAccess.Read)))
{
FileInfo info = new FileInfo("dsa.cs.msg");
message = reader.ReadBytes((int)info.Length);
}
//
// Load the signature
// Signature is (r,s)
//
byte[] signature = null;
using (BinaryReader reader = new BinaryReader(
new FileStream("dsa.cs.sig", FileMode.Open, FileAccess.Read)))
{
FileInfo info = new FileInfo("dsa.cs.sig");
signature = reader.ReadBytes((int)info.Length);
}
//
// Compute h(m)
//
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hash = sha.ComputeHash(message);
//
// Initialize Verifier
//
DSASignatureDeformatter verifier =
new DSASignatureDeformatter(dsa);
verifier.SetHashAlgorithm("SHA1");
if (verifier.VerifySignature(hash, signature))
{
UTF8Encoding utf8 = new UTF8Encoding();
String s = utf8.GetString(message);
MessageBox.Show("Message Verified. Recovered String:\n" + s);
}
else
{
MessageBox.Show("Message Not Verified.");
}
// See http://blogs.msdn.com/tess/archive/2007/10/31/
// asp-net-crash-system-security-cryptography-cryptographicexception.aspx
dsa.Clear();
}
private static void SignDsaMessage()
{
//
// Load the Private Key
// PKCS#8 Format
//
AsnKeyParser keyParser =
new AsnKeyParser("private.dsa.cs.key");
DSAParameters privateKey = keyParser.ParseDSAPrivateKey();
//
// Initailize the CSP
// Supresses creation of a new key
//
CspParameters csp = new CspParameters();
csp.KeyContainerName = "DSA Test (OK to Delete)";
// Cannot use PROV_DSS_DH
const int PROV_DSS = 3;
csp.ProviderType = PROV_DSS;
const int AT_SIGNATURE = 2;
csp.KeyNumber = AT_SIGNATURE;
//
// Initialize the Provider
//
DSACryptoServiceProvider dsa =
new DSACryptoServiceProvider(csp);
dsa.PersistKeyInCsp = false;
//
// The moment of truth...
//
dsa.ImportParameters(privateKey);
//
// Sign the Message
//
DSASignatureFormatter signer =
new DSASignatureFormatter(dsa);
signer.SetHashAlgorithm("SHA1");
// The one and only
String m = "Crypto Interop: \u9aa8";
byte[] message = Encoding.GetEncoding("UTF-8").GetBytes(m);
// h(m)
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] hash = sha.ComputeHash(message);
// Create the Signature for h(m)
byte[] signature = signer.CreateSignature(hash);
// Write the message
using (BinaryWriter writer = new BinaryWriter(
new FileStream("dsa.cs.msg", FileMode.Create,
FileAccess.ReadWrite)))
{
writer.Write(message);
}
// Write the signature on the message
using (BinaryWriter writer = new BinaryWriter(
new FileStream("dsa.cs.sig", FileMode.Create,
FileAccess.ReadWrite)))
{
writer.Write(signature);
}
// See http://blogs.msdn.com/tess/archive/2007/10/31/
// asp-net-crash-system-security-cryptography-cryptographicexception.aspx
dsa.Clear();
}
private static void CreateDsaKeys()
{
CspParameters csp = new CspParameters();
csp.KeyContainerName = "DSA Test (OK to Delete)";
const int PROV_DSS_DH = 13;
csp.ProviderType = PROV_DSS_DH;
const int AT_SIGNATURE = 2;
csp.KeyNumber = AT_SIGNATURE;
DSACryptoServiceProvider dsa =
new DSACryptoServiceProvider(1024, csp);
dsa.PersistKeyInCsp = false;
// Encoded key
AsnKeyBuilder.AsnMessage key = null;
// Private Key
DSAParameters privateKey = dsa.ExportParameters(true);
key = AsnKeyBuilder.PrivateKeyToPKCS8(privateKey);
using (BinaryWriter writer = new BinaryWriter(
new FileStream("private.dsa.cs.key", FileMode.Create,
FileAccess.ReadWrite)))
{
writer.Write(key.GetBytes());
}
// Public Key
DSAParameters publicKey = dsa.ExportParameters(false);
key = AsnKeyBuilder.PublicKeyToX509(publicKey);
using (BinaryWriter writer = new BinaryWriter(
new FileStream("public.dsa.cs.key", FileMode.Create,
FileAccess.ReadWrite)))
{
writer.Write(key.GetBytes());
}
// See http://blogs.msdn.com/tess/archive/2007/10/31/
// asp-net-crash-system-security-cryptography-cryptographicexception.aspx
dsa.Clear();
}
}
}
C#使用的key格式是一个xml,在项目包中有对应的类处理key,这里就不赘述了。值得注意的是c#不能使用java生成的密钥,而C++中是可以的。
总结
///
// 16进制string转为byte
///
string Hex2Byte(const string& s)
{
int sigLen = s.length();
unsigned char *bSig = new unsigned char[sigLen / 2 ];
int index = 0;
for (int i = 0; i < sigLen; i += 2, index++)
{
unsigned short byte = 0;
std::istringstream iss(s.substr(i, 2).c_str());
iss >> std::hex >> byte;
bSig[index] = byte;
}
return string((const char*)bSig, sigLen/2);
}
///
// 转为16进制string
///
string Byte2Hex(const string& s)
{
std::ostringstream ret;
for (string::size_type i = 0; i < s.length(); ++i)
{
ret << std::hex << std::setfill('0') << std::setw(2) << ((int)s[i] & 0xff);
}
return ret.str();
}