C# 无法从 OpenSSL 控制台行命令验证 ECDSA(384 位)base 64 数字签名
C# fails to verify ECDSA (384-bit) base 64 digital signature from OpenSSL console line commands
[版主,我在将这个问题挤入字数限制时遇到了问题,请见谅。]
用例是在 Linux 服务器上使用 OpenSSL 使用 384 位椭圆曲线数字服务器算法 (ECDSA) 对许可证(纯文本)文件进行签名,数字签名的验证发生在客户的 Windows 桌面 OS 运行ning full (Windows) .NET Framework。
许可证文件和 Base 64 编码的数字签名通过电子邮件发送给客户(不在共享公司网络上)。客户正在 运行使用 C# 编写的 .NET Framework(Windows 版)应用程序并验证许可证和数字签名以解锁 paid-for 功能。
现在,我说 Linux 但下面给出的示例服务器端代码还没有使用 Linux 脚本语言。我正在使用 VBA 运行ning 在 Windows 8 上制作原型,最终我将转换为 Linux 脚本语言,但暂时请耐心等待。
关键是我正在使用 OpenSSL 控制台命令,而不是针对任何 OpenSSL 软件开发工具包(C++ headers 等)进行编译。
一个棘手的部分(也许是开始代码审查的最佳位置)是从 DER 文件中挖掘出构成 public 键的 X 和 Y co-ordinates。 DER 密钥文件是使用抽象语法表示法 (ASN1) 的二进制编码文件,那里有免费的 GUI 程序,例如 Code Project ASN1. Editor 可以轻松检查,这里是 public 密钥文件的屏幕截图
幸运的是,OpenSSL 有自己内置的 ASN1 解析器,因此将相同的详细信息写入控制台,如下所示
C:\OpenSSL-Win64\bin\openssl.exe asn1parse -inform DER -in n:\ECDSA17-11-03T193106\ec_pubkey.der
0:d=0 hl=2 l= 118 cons: SEQUENCE
2:d=1 hl=2 l= 16 cons: SEQUENCE
4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
13:d=2 hl=2 l= 5 prim: OBJECT :secp384r1
20:d=1 hl=2 l= 98 prim: BIT STRING
所以在偏移量 20 处有 98 个字节包含 X 和 Y co-ordinates,在字节 20 处是一个标记 (0x03),指示后面是一个字符串,在字节 21 处是长度,98(任何127 以下的长度只需要一个字节)。所以实际上真正的 98 字节数据从字节 22 开始,所以我总共读取了 100 个字节 (98+2)。在字节 22 处是 0x00,这就是所谓的 BIT STRINGS begin (see Point 5). At byte 23 is 0x04 which indicates that both X and Y follow 未压缩形式(可以给出 X 值并计算 Y,在这种情况下使用 0x02 或 0x03)。 0x04 之后是 X 和 Y 坐标,每个 48 字节,因为 8 位在一个字节中,8*48=384.
于是挖出两个(X & Y)很长的十六进制数作为字符串。下一个难题是创建适合 C# 代码的 Xml 文件。关键 class 是 C# 的 ECDsaCng,导入方法是 FromXmlString,它希望文件实现标准 Rfc4050。 C# 的 ECDsaCng 导入的 Xml 文件要求 X 和 Y 是十进制而不是十六进制所以我们必须编写另一个函数来转换,我从另一种语言翻译过来 Stack Overflow question.
这是 VBA 代码(有很多),您需要更改它写入工作文件的位置。 运行 的两个代码块是 EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
和 EntryPoint2_RunHashAndSignBatch
应该理解为已经安装了OpenSSL,我的版本在C:\OpenSSL-Win64\
full VBA code is here 因为 SO 有 30000 个字符的限制。给出了可能的罪魁祸首代码
Option Explicit
Option Private Module
'******* Requires Tools->References to the following libraries
'* Microsoft ActiveX Data Objects 6.1 Library C:\Program Files (x86)\Common Files\System\ado\msado15.dll
'* Microsoft Scripting Runtime C:\Windows\SysWOW64\scrrun.dll
'* Microsoft XML, v.6.0 C:\Windows\SysWOW64\msxml6.dll
'* Windows Script HostObject Model C:\Windows\SysWOW64\wshom.ocx
'* Microsoft VBScript Regular Expressions 5.5 C:\Windows\SysWOW64\vbscript.dll
Private fso As New Scripting.FileSystemObject
Private Const sOPENSSL_BIN As String = "C:\OpenSSL-Win64\bin\openssl.exe" '* installation for OpenSSL
Private msBatchDir As Variant '* hold over so we can sign multiple times
Private Function ExportECDSAToXml(ByVal sPublicKeyFile As String, ByVal sXmlFile As String) As Boolean
'* C#'s ECDsaCng class has a FromXmlString method which imports public key from a xml file Rfc4050
'* In this subroutine we use OpenSSL's asn1parse command to determine where the X and Y coordinates
'* are to be found, we dig them out and then markup an Xml file
'* sample output
'<ECDSAKeyValue xmlns="http://www.w3.org/2001/04/xmldsig-more#">
' <DomainParameters>
' <NamedCurve URN="urn:oid:1.3.132.0.34" />
' </DomainParameters>
' <PublicKey>
' <X Value="28988690734503506507042353413239022820576378869683128926072865549806544603682841538004244894267242326732083660928511" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
' <Y Value="26760429725303641669535466935138151998536365153900531836644163359528872675820305636066450549811202036369304684551859" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
' </PublicKey>
'</ECDSAKeyValue>
Dim sAS1ParseCmd As String
sAS1ParseCmd = sOPENSSL_BIN & " asn1parse -inform DER -in " & sPublicKeyFile
Dim eAS1ParseStatus As WshExecStatus, sAS1ParseStdOut As String, sAS1ParseStdErr As String
eAS1ParseStatus = RunShellAndWait(sAS1ParseCmd, sAS1ParseStdOut, sAS1ParseStdErr)
Debug.Print sAS1ParseStdOut
'* sample output from standard out pipe is given blow.
'* we need to dig into the BIT STRING which is the final item
'* we need offset and length which is always 20 and 98 for 384 bit ECDSA
'* but I have written logic in case we want to upgrade to 512 or change of curve etc.
' 0:d=0 hl=2 l= 118 cons: SEQUENCE
' 2:d=1 hl=2 l= 16 cons: SEQUENCE
' 4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
' 13:d=2 hl=2 l= 5 prim: OBJECT :secp384r1
' 20:d=1 hl=2 l= 98 prim: BIT STRING
Dim vOutputSplit As Variant
vOutputSplit = VBA.Split(sAS1ParseStdOut, vbNewLine)
'* remove the traling blank line
If Trim(vOutputSplit(UBound(vOutputSplit))) = "" Then ReDim Preserve vOutputSplit(0 To UBound(vOutputSplit) - 1)
'* final line should be the long bit string, i.e. contain 'BIT STRING'
Debug.Assert StrComp("BIT STRING", Right$(Trim(vOutputSplit(UBound(vOutputSplit))), 10)) = 0
'* use regular expression to dig out offset and length
Dim lOffset As Long, lLength As Long
RegExpOffsetAndLengthFromASN1Parse Trim(vOutputSplit(UBound(vOutputSplit))), lOffset, lLength
Dim abytes() As Byte
Dim asHexs() As String '* for debugging
'* read in the whole file into a byte array
ReadFileBytesAsBytes sPublicKeyFile, abytes
'* for debugging create an array of hexadecimals
ByteArrayToHexStringArray abytes, asHexs
Dim bitString() As Byte
'* need extra 2 bytes because of leading type and length bytes
CopyArraySlice abytes, lOffset, lLength + 2, bitString()
'* some asserts which pin down structure of the bytes
Debug.Assert bitString(0) = 3 '* TAG for BIT STRING
Debug.Assert bitString(1) = lLength
'* From Point 5 at http://certificate.fyicenter.com/2221_View_Website_Server_Certificate_in_Google_Chrome.html
'* "ASN.1 BIT STRING value is stored with DER encoding as the value itself with an extra leading byte of 0x00. "
Debug.Assert bitString(2) = 0
'* 0x04 means by x and y values follow, i.e. uncompressed
'* (instead of just one from which the other can be derived, leading with 0x02 or 0x03)
'* https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
Debug.Assert bitString(3) = 4
'Stop
Dim x() As Byte
Dim y() As Byte
'* slice out the 48 bits for nopth x and y
'* why 48? because 48*8=384 bits(change for 512)
CopyArraySlice bitString, 4, 48, x()
CopyArraySlice bitString, 52, 48, y()
'* convert bytes to hex string for x coord
Dim sHexX As String
sHexX = ByteArrayToHexString(x(), "")
Debug.Print "sHexX:" & sHexX
'* convert bytes to hex string for y coord
Dim sHexY As String
sHexY = ByteArrayToHexString(y(), "")
Debug.Print "sHexY:" & sHexY
'* convert hexadeciumal to plain decimal
'* as Xml file requires it
Dim sDecX As String
sDecX = HexToDecimal(sHexX)
Debug.Print "sDecX:" & sDecX
'* convert hexadeciumal to plain decimal
'* as Xml file requires it
Dim sDecY As String
sDecY = HexToDecimal(sHexY)
Debug.Print "sDecY:" & sDecY
'* create the xml file from a template
Dim dom2 As MSXML2.DOMDocument60
Set dom2 = New MSXML2.DOMDocument60
dom2.LoadXML ECDSAXml(sDecX, sDecY)
Debug.Assert dom2.parseError.ErrorCode = 0
dom2.Save sXmlFile
Debug.Print dom2.XML
Set dom2 = Nothing
Debug.Assert CreateObject("Scripting.FileSystemObject").FileExists(sXmlFile)
End Function
这是 VBA 即时 window 的输出,它说明了控制台命令和 运行ning EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
.
的响应
Creating batch directory :n:\ECDSA17-11-03T193106
C:\OpenSSL-Win64\bin\openssl.exe ecparam -genkey -name secp384r1 -out n:\ECDSA17-11-03T193106\ec_key.pem
C:\OpenSSL-Win64\bin\openssl.exe ec -pubout -outform DER -in n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\ec_pubkey.der
C:\OpenSSL-Win64\bin\openssl.exe ec -pubout -outform PEM -in n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\ec_pubkey.pem
C:\OpenSSL-Win64\bin\openssl.exe ec -noout -text -in n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\ec_key.txt
Private-Key: (384 bit)
priv:
00:98:78:0d:c7:29:10:1c:9f:4d:75:b2:95:01:01:
a9:d2:36:72:0d:77:6a:5c:57:8d:51:a0:53:27:05:
9b:22:1c:c9:0a:1e:e1:27:06:92:c1:6c:2a:c4:bb:
46:91:98:f6
pub:
04:bd:4a:38:04:69:d5:ba:fa:11:27:0f:a8:ef:70:
3f:11:8d:e0:0f:e7:fd:26:ac:4d:40:32:7a:b5:9c:
97:71:c1:80:72:1b:42:25:f8:a4:49:4d:8f:89:bf:
1b:e9:6c:8c:f3:0b:02:db:89:b3:f7:92:e8:c4:a6:
ce:04:88:10:51:cc:17:0b:b8:9c:9a:a6:3d:fd:ec:
d4:99:c3:31:6b:22:1d:b6:41:fa:3c:0e:51:fe:86:
67:bb:7e:86:ce:06:6c
ASN1 OID: secp384r1
NIST CURVE: P-384
C:\OpenSSL-Win64\bin\openssl.exe asn1parse -inform DER -in n:\ECDSA17-11-03T193106\ec_pubkey.der
0:d=0 hl=2 l= 118 cons: SEQUENCE
2:d=1 hl=2 l= 16 cons: SEQUENCE
4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
13:d=2 hl=2 l= 5 prim: OBJECT :secp384r1
20:d=1 hl=2 l= 98 prim: BIT STRING
sHexX:BD4A380469D5BAFA11270FA8EF703F118DE00FE7FD26AC4D40327AB59C9771C180721B4225F8A4494D8F89BF1BE96C8C
sHexY:F30B02DB89B3F792E8C4A6CE04881051CC170BB89C9AA63DFDECD499C3316B221DB641FA3C0E51FE8667BB7E86CE066C
sDecX:29134384736743232303148959866907873847020585008044539704341734517362687803911673703523083044584737202030832217844876
sDecY:37407743276271579329804703064876533532537408218368858949720169306023437854945515421210341789026319167790678153234028
<ECDSAKeyValue xmlns="http://www.w3.org/2001/04/xmldsig-more#">
<DomainParameters>
<NamedCurve URN="urn:oid:1.3.132.0.34" />
</DomainParameters>
<PublicKey>
<X Value="28988690734503506507042353413239022820576378869683128926072865549806544603682841538004244894267242326732083660928511" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
<Y Value="26760429725303641669535466935138151998536365153900531836644163359528872675820305636066450549811202036369304684551859" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
</PublicKey>
</ECDSAKeyValue>
这是 VBA 即时 window 输出 运行ning EntryPoint2_RunHashAndSignBatch
...
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -out n:\ECDSA17-11-03T193106\license.sha256 n:\ECDSA17-11-03T193106\license.txt
SHA256(n:\ECDSA17-11-03T193106\license.txt)= 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -sign n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\license.sig n:\ECDSA17-11-03T193106\license.txt
C:\OpenSSL-Win64\bin\openssl.exe base64 -in n:\ECDSA17-11-03T193106\license.sig -out n:\ECDSA17-11-03T193106\license.sigb64
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -verify n:\ECDSA17-11-03T193106\ec_pubkey.pem -signature n:\ECDSA17-11-03T193106\license.sig n:\ECDSA17-11-03T193106\license.txt
Verification success
接下来我们创建一个 C# classic 控制台应用程序并粘贴以下代码以验证数字签名,记住客户将收到 base64 版本的数字签名。
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Xml;
namespace ECDSAVerSig
{
class Program
{
static Action<string> feedback { get; set; }
static byte[] fileContents = null;
static byte[] signatureContents = null;
static ECDsaCng client = null;
static HashAlgorithm hashAlgo = new SHA256Managed();
static String parentDirectory = null;
static void Main(string[] args)
{
//* the following will be different for you!!!
//* and will need to match what was output by the VBA script
parentDirectory = "n:\ECDSA\2017-11-03T193106\";
Debug.Assert(Directory.Exists(parentDirectory));
feedback = Console.WriteLine; // Abstract away
if (LoadSignature())
{
VerifySignature();
}
}
static private Boolean VerifySignature()
{
try
{
// a byte array to store hash value
byte[] hashedData = null;
Debug.Assert(fileContents[0] == 'H');
Debug.Assert(fileContents[1] == 'e');
Debug.Assert(fileContents[2] == 'l');
Debug.Assert(fileContents[3] == 'l');
Debug.Assert(fileContents[4] == 'o');
hashedData = hashAlgo.ComputeHash(fileContents);
//'* hard coded check of "Hello" hash
Debug.Assert(hashedData[0] == 0x18);
Debug.Assert(hashedData[1] == 0x5f);
//* the following is consistently wrong though it is my best guess
Boolean verified = client.VerifyHash(hashedData, signatureContents); //<-- Help required here Whosebugers
feedback("Verification:" + verified);
if (verified)
{
feedback("Hooray you got this 384 bit ECDSA code working! You absolute star!");
} else
{
feedback("Oh dear, still does not work. Please keep twiddling.");
}
Debug.Assert(verified);
return true;
}
catch (XmlException ex)
{
feedback("Problem with verification (Xml parse error):" + ex.ToString());
return false;
}
catch (Exception ex)
{
feedback("Problem with verification :" + ex.ToString());
return false;
}
}
static private Boolean LoadSignature()
{
client = new ECDsaCng();
try
{
System.Xml.XmlDocument dom = new System.Xml.XmlDocument();
dom.Load(Path.Combine(parentDirectory,"ec_pubkey.xml"));
string xml = dom.OuterXml;
feedback(xml);
client.FromXmlString(xml, ECKeyXmlFormat.Rfc4050);
fileContents = System.IO.File.ReadAllBytes(Path.Combine(parentDirectory, "license.txt"));
string base64SignatureContents = System.IO.File.ReadAllText(Path.Combine(parentDirectory, "license.sigB64"));
signatureContents = Convert.FromBase64String(base64SignatureContents);
byte[] hashedData = hashAlgo.ComputeHash(fileContents);
//'* hard coded check of "Hello" hash
Debug.Assert(hashedData[0] == 0x18);
Debug.Assert(hashedData[1] == 0x5f);
return true;
}
catch (XmlException ex)
{
feedback("Problem with reading digital signature (Xml parse error):" + ex.ToString());
return false;
}
catch (Exception ex)
{
feedback("Problem with reading digital signature:" + ex.ToString());
return false;
}
}
}
}
我已经对这段代码进行了三次检查。我已将许可证文件制作得非常短 "Hello" 并检查了字节和编码。我也检查了哈希值。我不知道下一步该怎么做。请协助。提前致谢
假设您正确地完成了所有其他操作 - 问题是 openssl 和 .NET 生成的 signatures 格式不同。由 openssl 生成(和预期)的签名是(惊喜!)再次使用 ASN.1 编码。 运行
openssl.exe asn1parse -in license.sig -inform DER
你会看到
0:d=0 hl=2 l= 101 cons: SEQUENCE
2:d=1 hl=2 l= 49 prim: INTEGER :F25556BBB... big number here
53:d=1 hl=2 l= 48 prim: INTEGER :3E98E7B376624FF.... big number
所以它又是两个数字的序列,(基于 0 的)索引 1 处的字节是总长度,索引 3 处的字节是第一个数字的长度,然后是第一个数字,在那个字节之后是第二个数字的长度,然后是第二个数字。请注意,可能涉及可选的填充(0 字节),应该将其删除,所以不要像我含糊地描述的那样实现它,而是阅读如何正确解析 ASN.1。
无论如何,.NET 期望这两个数字连接在一起,没有任何 ASN.1 内容,因此您再次需要提取它们。作为快速测试 - 获取您从上述命令输出中看到的这两个数字(它们是十六进制),连接在一起并将十六进制字符串转换为字节数组,然后在您的代码中用作 signatureContents
。或者,使用此示例代码(从不 使用它来真正提取这些数字)从您现有的签名中提取数字(如果使用此代码您仍然得到无效签名 - 尝试上面的复制方法数据直接来自 asn1parse 输出):
// only for testing purposes
private static byte[] FromOpenSslSignature(byte[] data) {
var rLength = data[3];
byte[] rData = new byte[48];
Array.Copy(data, 4 + (rLength - 48), rData, 0, 48);
var sLength = data[5 + rLength];
byte[] sData = new byte[48];
Array.Copy(data, 6 + rLength + (sLength - 48), sData, 0, 48);
return rData.Concat(sData).ToArray();
}
如果你做的一切都正确 - 签名会验证得很好。
[版主,我在将这个问题挤入字数限制时遇到了问题,请见谅。]
用例是在 Linux 服务器上使用 OpenSSL 使用 384 位椭圆曲线数字服务器算法 (ECDSA) 对许可证(纯文本)文件进行签名,数字签名的验证发生在客户的 Windows 桌面 OS 运行ning full (Windows) .NET Framework。
许可证文件和 Base 64 编码的数字签名通过电子邮件发送给客户(不在共享公司网络上)。客户正在 运行使用 C# 编写的 .NET Framework(Windows 版)应用程序并验证许可证和数字签名以解锁 paid-for 功能。
现在,我说 Linux 但下面给出的示例服务器端代码还没有使用 Linux 脚本语言。我正在使用 VBA 运行ning 在 Windows 8 上制作原型,最终我将转换为 Linux 脚本语言,但暂时请耐心等待。
关键是我正在使用 OpenSSL 控制台命令,而不是针对任何 OpenSSL 软件开发工具包(C++ headers 等)进行编译。
一个棘手的部分(也许是开始代码审查的最佳位置)是从 DER 文件中挖掘出构成 public 键的 X 和 Y co-ordinates。 DER 密钥文件是使用抽象语法表示法 (ASN1) 的二进制编码文件,那里有免费的 GUI 程序,例如 Code Project ASN1. Editor 可以轻松检查,这里是 public 密钥文件的屏幕截图
幸运的是,OpenSSL 有自己内置的 ASN1 解析器,因此将相同的详细信息写入控制台,如下所示
C:\OpenSSL-Win64\bin\openssl.exe asn1parse -inform DER -in n:\ECDSA17-11-03T193106\ec_pubkey.der
0:d=0 hl=2 l= 118 cons: SEQUENCE
2:d=1 hl=2 l= 16 cons: SEQUENCE
4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
13:d=2 hl=2 l= 5 prim: OBJECT :secp384r1
20:d=1 hl=2 l= 98 prim: BIT STRING
所以在偏移量 20 处有 98 个字节包含 X 和 Y co-ordinates,在字节 20 处是一个标记 (0x03),指示后面是一个字符串,在字节 21 处是长度,98(任何127 以下的长度只需要一个字节)。所以实际上真正的 98 字节数据从字节 22 开始,所以我总共读取了 100 个字节 (98+2)。在字节 22 处是 0x00,这就是所谓的 BIT STRINGS begin (see Point 5). At byte 23 is 0x04 which indicates that both X and Y follow 未压缩形式(可以给出 X 值并计算 Y,在这种情况下使用 0x02 或 0x03)。 0x04 之后是 X 和 Y 坐标,每个 48 字节,因为 8 位在一个字节中,8*48=384.
于是挖出两个(X & Y)很长的十六进制数作为字符串。下一个难题是创建适合 C# 代码的 Xml 文件。关键 class 是 C# 的 ECDsaCng,导入方法是 FromXmlString,它希望文件实现标准 Rfc4050。 C# 的 ECDsaCng 导入的 Xml 文件要求 X 和 Y 是十进制而不是十六进制所以我们必须编写另一个函数来转换,我从另一种语言翻译过来 Stack Overflow question.
这是 VBA 代码(有很多),您需要更改它写入工作文件的位置。 运行 的两个代码块是 EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
和 EntryPoint2_RunHashAndSignBatch
应该理解为已经安装了OpenSSL,我的版本在C:\OpenSSL-Win64\
full VBA code is here 因为 SO 有 30000 个字符的限制。给出了可能的罪魁祸首代码
Option Explicit
Option Private Module
'******* Requires Tools->References to the following libraries
'* Microsoft ActiveX Data Objects 6.1 Library C:\Program Files (x86)\Common Files\System\ado\msado15.dll
'* Microsoft Scripting Runtime C:\Windows\SysWOW64\scrrun.dll
'* Microsoft XML, v.6.0 C:\Windows\SysWOW64\msxml6.dll
'* Windows Script HostObject Model C:\Windows\SysWOW64\wshom.ocx
'* Microsoft VBScript Regular Expressions 5.5 C:\Windows\SysWOW64\vbscript.dll
Private fso As New Scripting.FileSystemObject
Private Const sOPENSSL_BIN As String = "C:\OpenSSL-Win64\bin\openssl.exe" '* installation for OpenSSL
Private msBatchDir As Variant '* hold over so we can sign multiple times
Private Function ExportECDSAToXml(ByVal sPublicKeyFile As String, ByVal sXmlFile As String) As Boolean
'* C#'s ECDsaCng class has a FromXmlString method which imports public key from a xml file Rfc4050
'* In this subroutine we use OpenSSL's asn1parse command to determine where the X and Y coordinates
'* are to be found, we dig them out and then markup an Xml file
'* sample output
'<ECDSAKeyValue xmlns="http://www.w3.org/2001/04/xmldsig-more#">
' <DomainParameters>
' <NamedCurve URN="urn:oid:1.3.132.0.34" />
' </DomainParameters>
' <PublicKey>
' <X Value="28988690734503506507042353413239022820576378869683128926072865549806544603682841538004244894267242326732083660928511" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
' <Y Value="26760429725303641669535466935138151998536365153900531836644163359528872675820305636066450549811202036369304684551859" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
' </PublicKey>
'</ECDSAKeyValue>
Dim sAS1ParseCmd As String
sAS1ParseCmd = sOPENSSL_BIN & " asn1parse -inform DER -in " & sPublicKeyFile
Dim eAS1ParseStatus As WshExecStatus, sAS1ParseStdOut As String, sAS1ParseStdErr As String
eAS1ParseStatus = RunShellAndWait(sAS1ParseCmd, sAS1ParseStdOut, sAS1ParseStdErr)
Debug.Print sAS1ParseStdOut
'* sample output from standard out pipe is given blow.
'* we need to dig into the BIT STRING which is the final item
'* we need offset and length which is always 20 and 98 for 384 bit ECDSA
'* but I have written logic in case we want to upgrade to 512 or change of curve etc.
' 0:d=0 hl=2 l= 118 cons: SEQUENCE
' 2:d=1 hl=2 l= 16 cons: SEQUENCE
' 4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey
' 13:d=2 hl=2 l= 5 prim: OBJECT :secp384r1
' 20:d=1 hl=2 l= 98 prim: BIT STRING
Dim vOutputSplit As Variant
vOutputSplit = VBA.Split(sAS1ParseStdOut, vbNewLine)
'* remove the traling blank line
If Trim(vOutputSplit(UBound(vOutputSplit))) = "" Then ReDim Preserve vOutputSplit(0 To UBound(vOutputSplit) - 1)
'* final line should be the long bit string, i.e. contain 'BIT STRING'
Debug.Assert StrComp("BIT STRING", Right$(Trim(vOutputSplit(UBound(vOutputSplit))), 10)) = 0
'* use regular expression to dig out offset and length
Dim lOffset As Long, lLength As Long
RegExpOffsetAndLengthFromASN1Parse Trim(vOutputSplit(UBound(vOutputSplit))), lOffset, lLength
Dim abytes() As Byte
Dim asHexs() As String '* for debugging
'* read in the whole file into a byte array
ReadFileBytesAsBytes sPublicKeyFile, abytes
'* for debugging create an array of hexadecimals
ByteArrayToHexStringArray abytes, asHexs
Dim bitString() As Byte
'* need extra 2 bytes because of leading type and length bytes
CopyArraySlice abytes, lOffset, lLength + 2, bitString()
'* some asserts which pin down structure of the bytes
Debug.Assert bitString(0) = 3 '* TAG for BIT STRING
Debug.Assert bitString(1) = lLength
'* From Point 5 at http://certificate.fyicenter.com/2221_View_Website_Server_Certificate_in_Google_Chrome.html
'* "ASN.1 BIT STRING value is stored with DER encoding as the value itself with an extra leading byte of 0x00. "
Debug.Assert bitString(2) = 0
'* 0x04 means by x and y values follow, i.e. uncompressed
'* (instead of just one from which the other can be derived, leading with 0x02 or 0x03)
'* https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm
Debug.Assert bitString(3) = 4
'Stop
Dim x() As Byte
Dim y() As Byte
'* slice out the 48 bits for nopth x and y
'* why 48? because 48*8=384 bits(change for 512)
CopyArraySlice bitString, 4, 48, x()
CopyArraySlice bitString, 52, 48, y()
'* convert bytes to hex string for x coord
Dim sHexX As String
sHexX = ByteArrayToHexString(x(), "")
Debug.Print "sHexX:" & sHexX
'* convert bytes to hex string for y coord
Dim sHexY As String
sHexY = ByteArrayToHexString(y(), "")
Debug.Print "sHexY:" & sHexY
'* convert hexadeciumal to plain decimal
'* as Xml file requires it
Dim sDecX As String
sDecX = HexToDecimal(sHexX)
Debug.Print "sDecX:" & sDecX
'* convert hexadeciumal to plain decimal
'* as Xml file requires it
Dim sDecY As String
sDecY = HexToDecimal(sHexY)
Debug.Print "sDecY:" & sDecY
'* create the xml file from a template
Dim dom2 As MSXML2.DOMDocument60
Set dom2 = New MSXML2.DOMDocument60
dom2.LoadXML ECDSAXml(sDecX, sDecY)
Debug.Assert dom2.parseError.ErrorCode = 0
dom2.Save sXmlFile
Debug.Print dom2.XML
Set dom2 = Nothing
Debug.Assert CreateObject("Scripting.FileSystemObject").FileExists(sXmlFile)
End Function
这是 VBA 即时 window 的输出,它说明了控制台命令和 运行ning EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
.
Creating batch directory :n:\ECDSA17-11-03T193106 C:\OpenSSL-Win64\bin\openssl.exe ecparam -genkey -name secp384r1 -out n:\ECDSA17-11-03T193106\ec_key.pem C:\OpenSSL-Win64\bin\openssl.exe ec -pubout -outform DER -in n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\ec_pubkey.der C:\OpenSSL-Win64\bin\openssl.exe ec -pubout -outform PEM -in n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\ec_pubkey.pem C:\OpenSSL-Win64\bin\openssl.exe ec -noout -text -in n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\ec_key.txt Private-Key: (384 bit) priv: 00:98:78:0d:c7:29:10:1c:9f:4d:75:b2:95:01:01: a9:d2:36:72:0d:77:6a:5c:57:8d:51:a0:53:27:05: 9b:22:1c:c9:0a:1e:e1:27:06:92:c1:6c:2a:c4:bb: 46:91:98:f6 pub: 04:bd:4a:38:04:69:d5:ba:fa:11:27:0f:a8:ef:70: 3f:11:8d:e0:0f:e7:fd:26:ac:4d:40:32:7a:b5:9c: 97:71:c1:80:72:1b:42:25:f8:a4:49:4d:8f:89:bf: 1b:e9:6c:8c:f3:0b:02:db:89:b3:f7:92:e8:c4:a6: ce:04:88:10:51:cc:17:0b:b8:9c:9a:a6:3d:fd:ec: d4:99:c3:31:6b:22:1d:b6:41:fa:3c:0e:51:fe:86: 67:bb:7e:86:ce:06:6c ASN1 OID: secp384r1 NIST CURVE: P-384 C:\OpenSSL-Win64\bin\openssl.exe asn1parse -inform DER -in n:\ECDSA17-11-03T193106\ec_pubkey.der 0:d=0 hl=2 l= 118 cons: SEQUENCE 2:d=1 hl=2 l= 16 cons: SEQUENCE 4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey 13:d=2 hl=2 l= 5 prim: OBJECT :secp384r1 20:d=1 hl=2 l= 98 prim: BIT STRING sHexX:BD4A380469D5BAFA11270FA8EF703F118DE00FE7FD26AC4D40327AB59C9771C180721B4225F8A4494D8F89BF1BE96C8C sHexY:F30B02DB89B3F792E8C4A6CE04881051CC170BB89C9AA63DFDECD499C3316B221DB641FA3C0E51FE8667BB7E86CE066C sDecX:29134384736743232303148959866907873847020585008044539704341734517362687803911673703523083044584737202030832217844876 sDecY:37407743276271579329804703064876533532537408218368858949720169306023437854945515421210341789026319167790678153234028 <ECDSAKeyValue xmlns="http://www.w3.org/2001/04/xmldsig-more#"> <DomainParameters> <NamedCurve URN="urn:oid:1.3.132.0.34" /> </DomainParameters> <PublicKey> <X Value="28988690734503506507042353413239022820576378869683128926072865549806544603682841538004244894267242326732083660928511" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" /> <Y Value="26760429725303641669535466935138151998536365153900531836644163359528872675820305636066450549811202036369304684551859" xsi:type="PrimeFieldElemType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" /> </PublicKey> </ECDSAKeyValue>
这是 VBA 即时 window 输出 运行ning EntryPoint2_RunHashAndSignBatch
...
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -out n:\ECDSA17-11-03T193106\license.sha256 n:\ECDSA17-11-03T193106\license.txt
SHA256(n:\ECDSA17-11-03T193106\license.txt)= 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -sign n:\ECDSA17-11-03T193106\ec_key.pem -out n:\ECDSA17-11-03T193106\license.sig n:\ECDSA17-11-03T193106\license.txt
C:\OpenSSL-Win64\bin\openssl.exe base64 -in n:\ECDSA17-11-03T193106\license.sig -out n:\ECDSA17-11-03T193106\license.sigb64
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -verify n:\ECDSA17-11-03T193106\ec_pubkey.pem -signature n:\ECDSA17-11-03T193106\license.sig n:\ECDSA17-11-03T193106\license.txt
Verification success
接下来我们创建一个 C# classic 控制台应用程序并粘贴以下代码以验证数字签名,记住客户将收到 base64 版本的数字签名。
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Xml;
namespace ECDSAVerSig
{
class Program
{
static Action<string> feedback { get; set; }
static byte[] fileContents = null;
static byte[] signatureContents = null;
static ECDsaCng client = null;
static HashAlgorithm hashAlgo = new SHA256Managed();
static String parentDirectory = null;
static void Main(string[] args)
{
//* the following will be different for you!!!
//* and will need to match what was output by the VBA script
parentDirectory = "n:\ECDSA\2017-11-03T193106\";
Debug.Assert(Directory.Exists(parentDirectory));
feedback = Console.WriteLine; // Abstract away
if (LoadSignature())
{
VerifySignature();
}
}
static private Boolean VerifySignature()
{
try
{
// a byte array to store hash value
byte[] hashedData = null;
Debug.Assert(fileContents[0] == 'H');
Debug.Assert(fileContents[1] == 'e');
Debug.Assert(fileContents[2] == 'l');
Debug.Assert(fileContents[3] == 'l');
Debug.Assert(fileContents[4] == 'o');
hashedData = hashAlgo.ComputeHash(fileContents);
//'* hard coded check of "Hello" hash
Debug.Assert(hashedData[0] == 0x18);
Debug.Assert(hashedData[1] == 0x5f);
//* the following is consistently wrong though it is my best guess
Boolean verified = client.VerifyHash(hashedData, signatureContents); //<-- Help required here Whosebugers
feedback("Verification:" + verified);
if (verified)
{
feedback("Hooray you got this 384 bit ECDSA code working! You absolute star!");
} else
{
feedback("Oh dear, still does not work. Please keep twiddling.");
}
Debug.Assert(verified);
return true;
}
catch (XmlException ex)
{
feedback("Problem with verification (Xml parse error):" + ex.ToString());
return false;
}
catch (Exception ex)
{
feedback("Problem with verification :" + ex.ToString());
return false;
}
}
static private Boolean LoadSignature()
{
client = new ECDsaCng();
try
{
System.Xml.XmlDocument dom = new System.Xml.XmlDocument();
dom.Load(Path.Combine(parentDirectory,"ec_pubkey.xml"));
string xml = dom.OuterXml;
feedback(xml);
client.FromXmlString(xml, ECKeyXmlFormat.Rfc4050);
fileContents = System.IO.File.ReadAllBytes(Path.Combine(parentDirectory, "license.txt"));
string base64SignatureContents = System.IO.File.ReadAllText(Path.Combine(parentDirectory, "license.sigB64"));
signatureContents = Convert.FromBase64String(base64SignatureContents);
byte[] hashedData = hashAlgo.ComputeHash(fileContents);
//'* hard coded check of "Hello" hash
Debug.Assert(hashedData[0] == 0x18);
Debug.Assert(hashedData[1] == 0x5f);
return true;
}
catch (XmlException ex)
{
feedback("Problem with reading digital signature (Xml parse error):" + ex.ToString());
return false;
}
catch (Exception ex)
{
feedback("Problem with reading digital signature:" + ex.ToString());
return false;
}
}
}
}
我已经对这段代码进行了三次检查。我已将许可证文件制作得非常短 "Hello" 并检查了字节和编码。我也检查了哈希值。我不知道下一步该怎么做。请协助。提前致谢
假设您正确地完成了所有其他操作 - 问题是 openssl 和 .NET 生成的 signatures 格式不同。由 openssl 生成(和预期)的签名是(惊喜!)再次使用 ASN.1 编码。 运行
openssl.exe asn1parse -in license.sig -inform DER
你会看到
0:d=0 hl=2 l= 101 cons: SEQUENCE
2:d=1 hl=2 l= 49 prim: INTEGER :F25556BBB... big number here
53:d=1 hl=2 l= 48 prim: INTEGER :3E98E7B376624FF.... big number
所以它又是两个数字的序列,(基于 0 的)索引 1 处的字节是总长度,索引 3 处的字节是第一个数字的长度,然后是第一个数字,在那个字节之后是第二个数字的长度,然后是第二个数字。请注意,可能涉及可选的填充(0 字节),应该将其删除,所以不要像我含糊地描述的那样实现它,而是阅读如何正确解析 ASN.1。
无论如何,.NET 期望这两个数字连接在一起,没有任何 ASN.1 内容,因此您再次需要提取它们。作为快速测试 - 获取您从上述命令输出中看到的这两个数字(它们是十六进制),连接在一起并将十六进制字符串转换为字节数组,然后在您的代码中用作 signatureContents
。或者,使用此示例代码(从不 使用它来真正提取这些数字)从您现有的签名中提取数字(如果使用此代码您仍然得到无效签名 - 尝试上面的复制方法数据直接来自 asn1parse 输出):
// only for testing purposes
private static byte[] FromOpenSslSignature(byte[] data) {
var rLength = data[3];
byte[] rData = new byte[48];
Array.Copy(data, 4 + (rLength - 48), rData, 0, 48);
var sLength = data[5 + rLength];
byte[] sData = new byte[48];
Array.Copy(data, 6 + rLength + (sLength - 48), sData, 0, 48);
return rData.Concat(sData).ToArray();
}
如果你做的一切都正确 - 签名会验证得很好。