[Moderators, I've had problems squeezing this question into the character limit, please be merciful.]
The use case is using OpenSSL on a Linux server to sign a license (plain text) file with a 384 bit Elliptic Curve Digital Server Algorithm (ECDSA), the verification of the digital signature takes place on a customer's Windows desktop OS running full (Windows) .NET Framework.
The license file and a Base 64 encoded digital signature are emailed to the customer (who is not on a shared corporate network). The customer is running a C# written .NET Framework (Windows edition) application and verification of the the licence and digital signature unlocks paid-for features.
Now, I say Linux but the example server side code given below is not yet in a Linux scripting language. I'm prototyping with VBA running on Windows 8, eventually I will convert over to a Linux scripting language but bear with me for the time being.
The point is I am using OpenSSL console commands and not compiling against any OpenSSL software development kit (C++ headers etc.).
One tricky part (and perhaps is the best place to begin code review) is the digging out of the X and Y co-ordinates that form the public key from the DER file. A DER key file is a binary encoded file that uses Abstract Syntax Notation (ASN1), there are free GUI programs out there such as Code Project ASN1. Editor that allow easy inspection, here is a screenshot of a public key file
Luckily, OpenSSL has its own inbuilt ASN1 parser so the same details are written to the console as the following
C:\OpenSSL-Win64\bin\openssl.exe asn1parse -inform DER -in n:\ECDSA\2017-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
So at offset 20 there are 98 bytes which contain the X and Y co-ordinates, at byte 20 is a tag (0x03) indicating a string is following and at byte 21 is the length, 98 (any length below 127 needs only one byte). So actually the real 98 bytes of data begin at byte 22 , so I read 100 bytes in total (98+2). At byte 22 is 0x00 which is how BIT STRINGS begin (see Point 5). At byte 23 is 0x04 which indicates that both X and Y follow which is known as uncompressed form (it is possible to give the X value and compute the Y in which case use 0x02 or 0x03). After the 0x04 comes the X and Y coordinates, 48 bytes each because 8 bits in a byte and 8*48=384.
So one digs out two (X & Y) very long hexadecimal numbers as strings. The next pain comes in creating the Xml file suitable for the C# code. The key class is C#'s ECDsaCng and the method to import is FromXmlString and it expected the file to implement standard Rfc4050. The Xml file that C#'s ECDsaCng imports requires X and Y to be in decimal and not hexadecimal so we have to write another function to convert, I translated from another language taken from another Stack Overflow question.
Here is the VBA code (there's quite a lot) and you'll need to change where it will write its working files. The two code blocks to run are EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
and EntryPoint2_RunHashAndSignBatch
It should be taken as read that OpenSSL has been installed, my version is at C:\OpenSSL-Win64\
The full VBA code is here because SO has a 30000 charact limit. The likely culprit code is given
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\3
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
Here is the output to the VBA immediate window which illustrates the console commands and the responses for running EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
.
Creating batch directory :n:\ECDSA\2017-11-03T193106 C:\OpenSSL-Win64\bin\openssl.exe ecparam -genkey -name secp384r1 -out n:\ECDSA\2017-11-03T193106\ec_key.pem C:\OpenSSL-Win64\bin\openssl.exe ec -pubout -outform DER -in n:\ECDSA\2017-11-03T193106\ec_key.pem -out n:\ECDSA\2017-11-03T193106\ec_pubkey.der C:\OpenSSL-Win64\bin\openssl.exe ec -pubout -outform PEM -in n:\ECDSA\2017-11-03T193106\ec_key.pem -out n:\ECDSA\2017-11-03T193106\ec_pubkey.pem C:\OpenSSL-Win64\bin\openssl.exe ec -noout -text -in n:\ECDSA\2017-11-03T193106\ec_key.pem -out n:\ECDSA\2017-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:\ECDSA\2017-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>
Here is the VBA immediate window output for running EntryPoint2_RunHashAndSignBatch
...
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -out n:\ECDSA\2017-11-03T193106\license.sha256 n:\ECDSA\2017-11-03T193106\license.txt
SHA256(n:\ECDSA\2017-11-03T193106\license.txt)= 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -sign n:\ECDSA\2017-11-03T193106\ec_key.pem -out n:\ECDSA\2017-11-03T193106\license.sig n:\ECDSA\2017-11-03T193106\license.txt
C:\OpenSSL-Win64\bin\openssl.exe base64 -in n:\ECDSA\2017-11-03T193106\license.sig -out n:\ECDSA\2017-11-03T193106\license.sigb64
C:\OpenSSL-Win64\bin\openssl.exe dgst -sha256 -verify n:\ECDSA\2017-11-03T193106\ec_pubkey.pem -signature n:\ECDSA\2017-11-03T193106\license.sig n:\ECDSA\2017-11-03T193106\license.txt
Verification success
Next we create a C# classic console application and paste in the following code to verify the digital signature remembering that the customer will be in receipt of a base64 version of the digital signature.
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 StackOverflowers
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;
}
}
}
}
I have triple checked this code. I've made the license file a very short "Hello" and checked bytes and encoding. I have checked the hashes as well. I do not know what next to do. Please assist. Thanks in advance
Assuming you did everything else correctly - problem is different formats of signatures produced by openssl and .NET. Signature produced (and expected) by openssl is (surprise!) again ASN.1 encoded. Run
openssl.exe asn1parse -in license.sig -inform DER
and you will see
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
So it's again sequence with two numbers, byte at (0-based) index 1 is total length, byte at index 3 is length of first number, then goes first number, after that byte with length of second number, then second number. Note that there might be optional padding involved (0 byte) which should be removed, so don't implement that as I vaguely described but read how to parse ASN.1 correctly.
Anyway, .NET expects these two numbers concatenated together, without any ASN.1 stuff, so you again need to extract them. As a quick test - grab these two numbers you see from the above command output (they are in hex), concatenate together and convert from hex string to byte array, then use in your code as signatureContents
. Alternatively, use this sample code (never use it for real extraction of those numbers) to extract numbers from your existing signature (if with this code you still get invalid signature - try approach above with copying data directly from asn1parse output):
// 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();
}
If you do everything correctly - signature will verify just fine.