Sunday, 26 November 2017

Use WinAPI (Crypt32) to convert string to Base64 from VBA

Following on from previous post which used MSXML to convert a string to a binary byte array base64 encoding. Here we abolish that dependency and go straight to the Windows API, specifically the Crypt32 module. Here again we make good use of StrConv.


Option Explicit
Option Private Module

Private Declare Function CryptBinaryToString Lib "Crypt32.dll" Alias _
                        "CryptBinaryToStringW" (ByRef pbBinary As Byte, _
                        ByVal cbBinary As Long, ByVal dwFlags As Long, _
                        ByVal pszString As Long, ByRef pcchString As Long) As Long

Private Declare Function CryptStringToBinary Lib "Crypt32.dll" Alias _
                        "CryptStringToBinaryW" (ByVal pszString As Long, _
                        ByVal cchString As Long, ByVal dwFlags As Long, _
                        ByVal pbBinary As Long, ByRef pcbBinary As Long, _
                        ByRef pdwSkip As Long, ByRef pdwFlags As Long) As Long


'* Refactored from vbforums.com -
'* VB6 - Base64 Encoding -
'* http://www.vbforums.com/showthread.php?850055-VB6-Base64-Encoding
'*
'* with thanks to users  "J.A. Coutts" and "LaVolpe"

Private Sub TestBase64Encode()
    
    Dim sPlainText As String
    sPlainText = "Hello world"
    
    Dim byt() As Byte
    byt = StrConv(sPlainText, vbFromUnicode)
    Dim sEncoded As String
    sEncoded = Base64Encode(byt)
    
    Dim sAnswer As String
    sAnswer = "SGVsbG8gd29ybGQ="
    Debug.Assert Len(sEncoded) = Len(sAnswer)
    Debug.Assert sEncoded = sAnswer
    'Dim lPos
    'For lPos = 1 To Len(sEncoded)
    '    Debug.Assert Mid$(sEncoded, lPos, 1) = Mid$(sEncoded, lPos, 1)
    'Next
    
    
    Dim bytDecoded() As Byte
    bytDecoded = Base64Decode(sEncoded)
    
    Dim sDecoded As String
    sDecoded = StrConv(bytDecoded, vbUnicode)
    
    Debug.Assert sPlainText = sDecoded
    
    Stop

End Sub


Private Function Base64Encode(ByRef byt() As Byte) As String
    Const CRYPT_STRING_BASE64 As Long = 1
    Const CBS As String = "CryptBinaryToString"
    Const Routine As String = "Base64.Base64Encode"
    Dim lLen As Long
    'Determine Base64 output String length required.
    If CryptBinaryToString(byt(0), UBound(byt) + 1, CRYPT_STRING_BASE64, StrPtr(vbNullString), lLen) = 0 Then
        'RaiseEvent Error(Err.LastDllError, CBS, Routine)
        Err.Raise Err.LastDllError, CBS, Routine
        GoTo ReleaseHandles
    End If
    'Convert binary to Base64.
    Dim sBase64Buf As String
    sBase64Buf = String$(lLen - 1, Chr$(0))
    If CryptBinaryToString(byt(0), UBound(byt) + 1, CRYPT_STRING_BASE64, StrPtr(sBase64Buf), lLen) = 0 Then
        'RaiseEvent Error(Err.LastDllError, CBS, Routine)
        Err.Raise Err.LastDllError, CBS, Routine
        GoTo ReleaseHandles
    End If
    Base64Encode = Left$(sBase64Buf, lLen - 2)
ReleaseHandles:
End Function

Private Function Base64Decode(ByVal sBase64Buf As String) As Byte()
    Const CRYPT_STRING_BASE64 As Long = 1
    Const CSB As String = "CryptStringToBinary"
    Const Routine As String = "Base64.Base64Decode"
    
    Const CRYPT_STRING_NOCRLF As Long = &H40000000
    
    Dim bTmp() As Byte
    Dim lLen As Long
    Dim dwActualUsed As Long
    'Get output buffer length
    If CryptStringToBinary(StrPtr(sBase64Buf), Len(sBase64Buf), CRYPT_STRING_BASE64, StrPtr(vbNullString), lLen, 0&, dwActualUsed) = 0 Then
        'RaiseEvent Error(Err.LastDllError, CSB, Routine)
        Err.Raise Err.LastDllError, CSB, Routine
        GoTo ReleaseHandles
    End If
    'Convert Base64 to binary.
    ReDim bTmp(lLen - 1)
    If CryptStringToBinary(StrPtr(sBase64Buf), Len(sBase64Buf), CRYPT_STRING_BASE64, VarPtr(bTmp(0)), lLen, 0&, dwActualUsed) = 0 Then
        'RaiseEvent Error(Err.LastDllError, CSB, Routine)
        Err.Raise Err.LastDllError, CSB, Routine
        GoTo ReleaseHandles
    Else

        Base64Decode = bTmp
    End If
ReleaseHandles:
End Function


Use MSXML2 to encode bytes or String to Base64

So I want to be able to decode and encode a byte array to and from base64. A canonical answer is given at SO (thanks!) but it converts to and from strings; the byte array element is buried in the code. I would like the option to pass byte arrays and so I have rearranged the code to make this more explicit. Also we use StrConv instead of the ADO.Stream trick. Here is my version.


Option Explicit
Option Private Module

'* Tools->References
'* Microsoft XML, v6.0


'* External Docs
'* MSDN - How to Encode XML Data  - https://msdn.microsoft.com/en-us/library/aa468560.aspx
'* MSDN - nodeTypedValue Property - https://msdn.microsoft.com/en-us/library/ms762308(v=vs.85).aspx
'* SO - Base64 Encode String in VBScript - https://stackoverflow.com/questions/496751/base64-encode-string-in-vbscript#answer-506992

Private Sub TestBase64Encode()
    Dim sOriginal As String
    sOriginal = "Hello world"
    
    Dim sBase64 As String
    sBase64 = Base64EncodeString(sOriginal)
    
    Dim sDecoded As String
    sDecoded = Base64DecodeString(sBase64)
    
    Debug.Assert sOriginal = sDecoded

End Sub


Function Base64EncodeString(ByVal sText As String) As String
    Dim byt() As Byte
    
    byt = VBA.StrConv(sText, VbStrConv.vbFromUnicode, 1033)
    
    Base64EncodeString = Base64EncodeFromBytes(byt)
End Function



Function Base64EncodeFromBytes(ByRef byt() As Byte) As String

    Dim oXML  As MSXML2.DOMDocument60
    Set oXML = New MSXML2.DOMDocument60
    
    Dim oNode As MSXML2.IXMLDOMNode

    Set oNode = oXML.createElement("base64")
    oNode.DataType = "bin.base64"
    
    oNode.nodeTypedValue = byt
    Base64EncodeFromBytes = oNode.Text
    
    Debug.Assert TypeName(Base64EncodeFromBytes) = "String"
    Set oNode = Nothing
    Set oXML = Nothing
End Function


Function Base64DecodeString(ByVal sText As String) As String

    Dim byt() As Byte
    byt = Base64DecodeToBytes(sText)
    

    Base64DecodeString = VBA.StrConv(byt(), VbStrConv.vbUnicode, 1033)

End Function

Function Base64DecodeToBytes(ByVal sEncoded As String) As Byte()
    
    Debug.Assert TypeName(sEncoded) = "String"
    
    Dim oXML  As MSXML2.DOMDocument60
    Set oXML = New MSXML2.DOMDocument60
    
    Dim oNode As MSXML2.IXMLDOMNode
    Set oNode = oXML.createElement("base64")
    
    oNode.DataType = "bin.base64"
    oNode.Text = sEncoded
    
    Base64DecodeToBytes = oNode.nodeTypedValue
    
    Set oNode = Nothing
    Set oXML = Nothing
End Function


Friday, 24 November 2017

VBScript file to unzip a zip file

My main development machine continues to have problems, it is very slow. This is quite a challenge. I need to download tools to fix but these downloads are zipped. Right-clicking and selecting Extract All now no longer works. Excel VBA and Word VBA are currently broken. So I need to unzip a file using a VBScript file. Here it is.



Extract "c:\Users\Simon\Downloads\PendMoves.zip"

Private Sub Extract (sItemName)

    Dim objFSO 'As New Scripting.FileSystemObject
    set objFSO = CreateObject("Scripting.FileSystemObject")

    if not objFSO.FileExists(sItemName) then
 Wscript.Echo  sItemName + " does not exist"
    else
     Wscript.Echo  sItemName

 dim oFile 
        set oFile = objFSO.GetFile(sItemName)
 
 

        Dim sFullItemName 'As String
        sFullItemName = objFSO.GetAbsolutePathName(sItemName)
 
 dim sName
        sName = objFSO.GetFile(sFullItemName).name 

 dim sLeafName
        sLeafName = split(sName,".")(0)

 dim sParentFolder 
 sParentFolder = oFile.ParentFolder.path

 
 dim sExtractFolder 
 sExtractFolder = objFSO.BuildPath(sParentFolder,sLeafName)

        Wscript.Echo "sParentFolder: " + sParentFolder + "   sLeafname: " + sLeafname + "  sExtractFolder:" + sExtractFolder

 dim oExtractFolder
 if not objFSO.FolderExists(sExtractFolder ) then
     dim oParentFolder 
     Set oParentFolder =  objFSO.GetFolder(sParentFolder)

     set oExtractFolder = oParentFolder.SubFolders.Add(sLeafName)

     'Wscript.Echo "created extract folder: " + sExtractFolder


        else
     'Wscript.Echo "acquiring extract folder: " + sExtractFolder             
     set oExtractFolder = objFSO.GetFolder(sExtractFolder )

        end if

 if not oExtractFolder is nothing then

     'Wscript.Echo "acquired extract folder: " + sExtractFolder       
            
            Dim oApp
            set oApp = CreateObject("Shell.Application")

            oApp.Namespace(sExtractFolder).CopyHere oApp.namespace(sItemName).items
            
            Wscript.Echo "copied to extract folder: " + sExtractFolder             

        end if



    end if

End Sub


Tuesday, 21 November 2017

Patch Tuesday Nightmare - "We're sorry, but Excel has run into an error ... would you like us to repair now?"

Every Month I dread Patch Tuesday where Microsoft forces me to take updates because after the update the virus scanner runs and the whole computer is slow. This month took a nasty turn and my main development PC is running very slow for shell operations such as browsing the Control Panel (which is broken) or getting right click menu options up in Windows Explorer.

Another nasty symptom post patch is being told that "We're sorry, but Excel has run into an error that is preventing it from working correctly. Excel will need to be closed as a result. Would you like us to repair now?

Here is the dialog box screenshot

If one takes the repair Excel then works.

Nevertheless this really does contribute to the sense that this month's patch (Nov 2017) is rogue!

C'mon Microsoft don't break my computer!

Note Patch Tuesday was last week and tried rolling back some of the patches to reverse my difficulties such as the following...

Install Date LinkRestart timeResult
16/11/2017 KB4048958 Restarted 14:58 Result:Not fixed
16/11/2017 KB4041777 Restarted 16:33 Result:Not fixed
16/11/2017 KB2976978 Restarted 17:25 Result:Not fixed
15/11/2017 KB4048951 Restarted 19:33 Result:Not fixed

After rolling back the above (to no relief), today Microsoft insisted upon reinstalling. Good grief!

To run the Add or Remove Programs control panel app without browsing the Control Panel (which is broken for me) use from a command line 'control.exe appwiz.cpl'

Monday, 20 November 2017

Use Shell API to VBA Script OneDrive Sync

So my computer is very slow in the Windows Explorer and I was looking to use the OneDrive (SkyDrive) to backup files. To force a synchronisation one right clicks on the OneDrive icon and selects 'Sync' but this menu is very very slow to appear to for me currently and I was wondering if it could be coded in VBA.

I found a StackOverflow answer which points to a nice JavaScript file on GitHub that uses the 'Microsoft Shell Controls and Automation' type library (albeit late binding). I have converted the code to VBA. The code gets the OneDrive folder item, and uses the Verbs method to get the list that appears when one right clicks to get context menu. To mimic pressing a menu option one calls DoIt on the Verb object.

I am curious to investigate how to add functionality to the Shell namespace now that I have discovered how to invoke such functionality. In the meantime here is some code.


Option Explicit
Option Private Module

'*Tools->References
'* Microsoft Scripting Runtime                      C:\Windows\sysWOW64\scrrun.dll
'* Microsoft Shell Controls and Automation          C:\Windows\sysWOW64\shell32.dll

Private Sub TestSyncItem()
    SyncItem ""
End Sub

Private Sub SyncItem(Optional ByVal sItemName As String)

    '* Based on https://github.com/npocmaka/batch.scripts/blob/master/hybrids/jscript/oneDriveSync.bat

    Dim objFSO As New Scripting.FileSystemObject
    Dim objShell As New Shell32.Shell  'ActiveXObject("Shell.Application");
    
    If LenB(sItemName) = 0 Then sItemName = GetOneDrivePathFromReg

    
    If objFSO.FolderExists(sItemName) Or objFSO.FileExists(sItemName) Then
    
        Dim sFullItemName As String
        sFullItemName = objFSO.GetAbsolutePathName(sItemName)
        
        Dim sNamespace As String
        sNamespace = objFSO.GetParentFolderName(sFullItemName)
        
        Dim sName As String
        sName = objFSO.GetFolder(sFullItemName).name
    
        '* Introduction to the Shell Namespace
        '* https://msdn.microsoft.com/en-us/library/windows/desktop/cc144090(v=vs.85).aspx
        Dim objFolder As Shell32.Folder
        Set objFolder = objShell.namespace(sNamespace)
        
    
        Dim objItem As Shell32.FolderItem
        Set objItem = objFolder.ParseName(sName)
        
        Dim oFolderItemVerb As Shell32.FolderItemVerb
        Set oFolderItemVerb = Nothing
        
        Dim vVerbLoop As Variant
        For Each vVerbLoop In objItem.Verbs
            If vVerbLoop.name = "Sync" Then
                Set oFolderItemVerb = vVerbLoop
                Exit For
            End If
        
        Next vVerbLoop
        If Not oFolderItemVerb Is Nothing Then
            Call oFolderItemVerb.DoIt
        
        End If
        Stop
    End If
    
End Sub

Private Function GetOneDrivePathFromReg() As String
    Const HKCU As Long = &H80000001
    
    Dim registryObject As Object
    Set registryObject = VBA.GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & _
             ".\root\default:StdRegProv")
    
    Dim sRet As String
    registryObject.GetStringValue HKCU, "Software\Microsoft\OneDrive", "UserFolder", sRet
    GetOneDrivePathFromReg = sRet
End Function




Tuesday, 14 November 2017

Use CreateObject with CLSID (when ProgID is unavailable)

So CreateObject usually takes a ProgId which is a human language string which is then looked up in the registry. However, sometimes the ProgId is not available, only a clsid is available. In such cases, it is still possible to instantiate the class using CreateObject one uses the syntax below.


Option Explicit

Sub Test()
    
    Dim dic As Object
    'Set dic = CreateObject("Scripting.Dictionary")
    Set dic = CreateObject("new:{EE09B103-97E0-11CF-978F-00A02463E06F}")
    
    dic.Add "blue", 5
    dic.Add "red", 7
    dic.Add "green", 11

    Debug.Assert dic.Count = 3

End Sub

Friday, 3 November 2017

Use VBA to shell OpenSSL console commands

I've got to ask a long and involved StackOverflow question and paste a load of code but first I'll paste it here and use this as a drafting board.

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 ASN1. Editor on the Code Project that allows 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\



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
'* Microsoft Script Control 1.0                         C:\Windows\SysWOW64\msscript.ocx

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 Property Get BatchDir() As String
    '* the point of this property procedure is to interpret the contents of
    '* the module level variable msBatchDir, which is a Variant and which
    '* when empty signals not yet set.  If not yet set then we return a default
    '* for testing convenience

    If IsEmpty(msBatchDir) Then
        '* place your favourite default here
        msBatchDir = "n:\ECDSA\2017-11-05T225657\"
    End If
    BatchDir = msBatchDir
End Property

Private Sub TestLoop()

    Dim l
    For l = 1 To 3
        EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce
        EntryPoint2_RunHashAndSignBatch
    Next l

End Sub


'* KEY ENTRY POINT 1 OF 2
Private Sub EntryPoint1_RunECDSAKeyGenerationBatch_RunOnce()
    '* Notes:
    '* during development I felt it convenient to start afresh whilst
    '* experimenting with different flags.  The best way to start afresh I
    '* found was to give myself a new directory for each run
    '* so here you'll find we create a directory based on timestamp
    


    '* you'll need to change this and create your own folder
    Const sROOT_DIRECTORY As String = "n:\ECDSA\"
    
    '* create a timestamped and thus unique folder to do our work in
    msBatchDir = sROOT_DIRECTORY & Format(Now(), "yyyy-MM-ddTHHnnss")
    Debug.Print "Creating batch directory :" & msBatchDir
    
    '* zap anything previous (unlikely)
    If fso.FolderExists(msBatchDir) Then fso.DeleteFolder msBatchDir
    fso.CreateFolder msBatchDir
    
    Dim bReturn As Boolean
    bReturn = RunECDSAKeyGenerationBatch_RunOnce(msBatchDir)
    
    
End Sub

'* KEY ENTRY POINT 2 OF 2
Private Sub EntryPoint2_RunHashAndSignBatch()
    
    '* using the same folder as the license key creation logic
    Dim sBatchDir As String
    sBatchDir = BatchDir()
    
    Dim sSignableFile As String
    sSignableFile = fso.BuildPath(sBatchDir, "license.txt")
    
    
    
    '* we write a trivial 5 bytes to a file
    '* as a simple substitute for our license file
    Dim bReturn As Boolean
    
    If Not True Then
        bReturn = WriteStringToFile("Hello World", sSignableFile)
    
    Else
        Dim sRandomText As String
        sRandomText = RandomText2
        
        Debug.Print sRandomText
        bReturn = WriteStringToFile(sRandomText, sSignableFile)
    
    End If
    

    If bReturn Then
    
        '* we need a file to contain bothe private and public keys
        '* this will live on the server
        Dim sKeyFile As String
        sKeyFile = fso.BuildPath(sBatchDir, "ec_key.pem")
    
        '* we need a file that contains only the public keys
        '* so we can distribute to customers for digital
        '* signature verification
        Dim sPublicKeyFile As String
        sPublicKeyFile = fso.BuildPath(sBatchDir, "ec_pubkey.pem")
    
        '* hash file was for debugging, not stricly necessary
        Dim sHashFile As String
        sHashFile = fso.BuildPath(sBatchDir, "license.sha256")
    
        '* the binary version of the license signature
        '* this is an interim file because not good for emails
        Dim sSignatureFile As String
        sSignatureFile = fso.BuildPath(sBatchDir, "license.sig")
    
        '* this base 64 version of license.sig allows contents to be pasted into
        '* an email.
        Dim sSignatureFileBase64 As String
        sSignatureFileBase64 = fso.BuildPath(sBatchDir, "license.sigb64")
    
    
    
        bReturn = RunHashAndSignBatch(sKeyFile, sPublicKeyFile, sSignableFile, sHashFile, sSignatureFile, sSignatureFileBase64)
    
        Debug.Assert bReturn
        
        RunCSharp "", "", ""
        
    End If
End Sub

Private Function RandomText2() As String
    
    'http://www.randomtext.me/api/
    Dim oXHR As MSXML2.XMLHTTP60
    Set oXHR = New MSXML2.XMLHTTP60
    
    'oXHR.Open "GET", "http://www.randomtext.me/api/"
    oXHR.Open "GET", "http://www.randomtext.me/api/gibberish/p-5/25-45"
    '#
    oXHR.send
    
    Dim oParseJSON As Object
    Set oParseJSON = ParseJSON(oXHR.responseText)
    
    RandomText2 = VBA.CallByName(oParseJSON, "text_out", VbGet)
    
End Function

Function ParseJSON(ByVal sJSON As String) As Object

    '* Tools->References->Microsoft Script Control 1.0   (msscript.ocx)
    Static oScriptControl As MSScriptControl.ScriptControl
    If oScriptControl Is Nothing Then
        Set oScriptControl = New MSScriptControl.ScriptControl
        oScriptControl.Language = "javascript"
    End If
    
    Dim oParsed As Object
    Set ParseJSON = oScriptControl.Eval("(" & sJSON & ")")
End Function


Private Function RunECDSAKeyGenerationBatch_RunOnce(ByVal sBatchDir As String) As Boolean

    '* generates paths for all our files
    Dim sECDSA_KEYFILE As String
    sECDSA_KEYFILE = fso.BuildPath(sBatchDir, "ec_key.pem")
    
    '* this is a pretty print file for debugging where
    '* OpenSSL outputs in a hexadecimal
    Dim sECDSA_KEYFILE_Text As String
    sECDSA_KEYFILE_Text = fso.BuildPath(sBatchDir, "ec_key.txt")
    
    '* the PEM files is in a text format cut and pastable into an email
    Dim sECDSA_PublicKeyPemFile As String
    sECDSA_PublicKeyPemFile = fso.BuildPath(sBatchDir, "ec_pubkey.pem")
    
    '* the DER file is required for debugging verification on the server
    '* and not strictly required for production
    Dim sECDSA_PublicKeyDerFile As String
    sECDSA_PublicKeyDerFile = fso.BuildPath(sBatchDir, "ec_pubkey.der")
    
    '* generate the keys by calling shelling subroutine
    Dim bReturn As Boolean
    bReturn = OpenSSL_GenECDSAKeys(sECDSA_KEYFILE, sECDSA_PublicKeyDerFile, sECDSA_PublicKeyPemFile)

    If bReturn Then

        '* only for debugging do we output the private key in plain text
        If Len(Trim(sECDSA_KEYFILE_Text)) > 0 Then
            
            '* for debugging generate the text "pretty print"
            OpenSSL_PrettyPrintKey sECDSA_KEYFILE, sECDSA_KEYFILE_Text
        End If

        '* The ECDsaCng C# class has a FromXmlString import method
        '* which consumes an Xml file conforming to standard Rfc4050
        Dim sECDSA_XmlExport As String
        sECDSA_XmlExport = fso.BuildPath(sBatchDir, "ec_pubkey.xml")
        
        bReturn = ExportECDSAToXml(sECDSA_PublicKeyDerFile, sECDSA_XmlExport)


    End If
    RunECDSAKeyGenerationBatch_RunOnce = bReturn
End Function



Private Function RunHashAndSignBatch(ByVal sKeyFile As String, ByVal sPublicKeyFile As String, ByVal sSignableFile As String, _
                                    ByVal sHashFile As String, ByVal sSignatureFile As String, ByVal sSignatureFileBase64 As String) As Boolean
    
    '* the hash is for debugging, not strictly necessary
    Dim bReturn As Boolean
    bReturn = OpenSSL_Hash(sKeyFile, sSignableFile, sHashFile)
    

    If bReturn Then
        '* we really need the base64 signature so we can cut and paste
        '* contents into an email
        bReturn = OpenSSL_SignDigest(sKeyFile, sSignableFile, sSignatureFile, sSignatureFileBase64)
    
    
        If bReturn Then
            '* as a debugging/sanity check we verify the signature
            bReturn = OpenSSL_VerifySignature(sPublicKeyFile, sSignatureFile, sSignableFile)
        
        End If
    
    
    End If

    RunHashAndSignBatch = bReturn
End Function




Private Function OpenSSL_PrettyPrintKey(ByVal sEcdsaKeyFile As String, _
                                    ByVal sEcdsaPrettyPrintFile As String) As Boolean
                                    
    '* ordinarily OpenSSL spits out a binary file but if yopu want to inspect the contents
    '* in a text file viewer then use this routine to create a viewable file
    
    '* sample output ...
    
    'Private-Key: (384 bit)
    'priv:
    '    54:ba:af:0d:dd:65:15:96:c6:33:ad:c8:2e:b5:3f:
    '    d3:c4:80:59:d3:50:9d:81:71:ea:bd:49:b5:14:cc:
    '    6a:e2:d9:43:1f:48:d9:35:ec:00:fa:5d:71:be:7a:
    '    c6:0a:a7
    'pub:
    '    04:1b:aa:e5:53:22:cd:d0:d7:da:fd:da:35:46:5a:
    '    ed:d3:0c:b3:6c:04:ad:69:3a:a3:3e:3a:47:ea:1e:
    '    9a:ca:9d:5b:7a:7d:29:f3:84:bd:b2:df:d4:79:de:
    '    8e:e7:1b:68:b7:61:b7:32:54:78:ef:5f:9b:8c:4f:
    '    a3:73:50:cf:1b:90:7c:2e:cf:24:5e:aa:54:ad:19:
    '    82:b6:26:f3:86:df:b6:d4:4a:45:01:02:76:fc:44:
    '    86:58:ee:eb:6d:f3:db
    'ASN1 OID: secp384r1
    'NIST CURVE: P -384
                                    
                                    
                                    
    Debug.Assert fso.FileExists(sOPENSSL_BIN)
    Debug.Assert fso.FileExists(sEcdsaKeyFile)
    
    If fso.FileExists(sEcdsaPrettyPrintFile) Then fso.DeleteFile sEcdsaPrettyPrintFile
    Debug.Assert Not fso.FileExists(sEcdsaPrettyPrintFile)
    
    Dim sPrettyPrintKeyCmd As String
    sPrettyPrintKeyCmd = sOPENSSL_BIN & " ec -noout -text -in " & sEcdsaKeyFile & " -out " & sEcdsaPrettyPrintFile
    
    Dim ePrettyPrintKeyStatus As WshExecStatus, sPrettyPrintKeyStdOut As String, sPrettyPrintKeyStdErr As String
    ePrettyPrintKeyStatus = RunShellAndWait(sPrettyPrintKeyCmd, sPrettyPrintKeyStdOut, sPrettyPrintKeyStdErr)
    
    If ePrettyPrintKeyStatus = WshFailed Then
        Debug.Print sPrettyPrintKeyStdErr
    Else
        Debug.Print sPrettyPrintKeyStdOut
        Debug.Assert fso.FileExists(sEcdsaPrettyPrintFile)
        Debug.Print fso.OpenTextFile(sEcdsaPrettyPrintFile).ReadAll
        OpenSSL_PrettyPrintKey = True
    End If
        'Stop
    
    

End Function

'

Private Function RunCSharp(ByVal sEcdsaKeyFile As String, _
                                    ByVal sEcdsaPublicKeyDerFile As String, _
                                    ByVal sEcdsaPublicKeyPemFile As String) As Boolean

    '* this routine generates the key pair (private and public) file which resides on
    '* the server and also the public key only file (in two formats here for debugging)
    Const sCSHARP As String = "C:\Users\Simon\Documents\Visual Studio 2017\Projects\ECDSAVerifySignature\ECDSAVerifySignature\bin\Debug\ECDSAVerifySignature.exe"
    Debug.Assert fso.FileExists(sCSHARP)
    
    Dim sBatchDir As String
    sBatchDir = BatchDir()
    Debug.Assert fso.FolderExists(sBatchDir)

    Dim sWinFile As String
    sWinFile = fso.BuildPath(sBatchDir, "win.txt")
    
    If fso.FileExists(sWinFile) Then fso.DeleteFile sWinFile
    Debug.Assert Not fso.FileExists(sWinFile)

    Dim sCSharpCmd As String
    sCSharpCmd = sCSHARP & " " & sBatchDir

    Dim eCSharpStatus As WshExecStatus, sCSharpStdOut As String, sCSharpStdErr As String
    eCSharpStatus = RunShellAndWait(sCSharpCmd, sCSharpStdOut, sCSharpStdErr)
    
    If eCSharpStatus = WshFailed Then
        Debug.Print sCSharpStdErr
    Else
        Debug.Print sCSharpStdOut
        'Debug.Assert fso.FileExists(sWinFile)
    End If

End Function


Private Function OpenSSL_GenECDSAKeys(ByVal sEcdsaKeyFile As String, _
                                    ByVal sEcdsaPublicKeyDerFile As String, _
                                    ByVal sEcdsaPublicKeyPemFile As String) As Boolean

    '* this routine generates the key pair (private and public) file which resides on
    '* the server and also the public key only file (in two formats here for debugging)

    Debug.Assert fso.FileExists(sOPENSSL_BIN)

    If fso.FileExists(sEcdsaKeyFile) Then fso.DeleteFile sEcdsaKeyFile
    Debug.Assert Not fso.FileExists(sEcdsaKeyFile)

    Dim sGenKeyCmd As String
    sGenKeyCmd = sOPENSSL_BIN & " ecparam -genkey -name secp384r1 -out " & sEcdsaKeyFile

    Dim eGenKeyStatus As WshExecStatus, sGenKeyStdOut As String, sGenKeyStdErr As String
    eGenKeyStatus = RunShellAndWait(sGenKeyCmd, sGenKeyStdOut, sGenKeyStdErr)
    
    If eGenKeyStatus = WshFailed Then
        Debug.Print sGenKeyStdErr
    Else
        Debug.Print sGenKeyStdOut
        Debug.Assert fso.FileExists(sEcdsaKeyFile)
        'Stop
    
        If fso.FileExists(sEcdsaPublicKeyDerFile) Then fso.DeleteFile sEcdsaPublicKeyDerFile
        Debug.Assert Not fso.FileExists(sEcdsaPublicKeyDerFile)
    
        Dim sPublicKeyDerCmd As String
        sPublicKeyDerCmd = sOPENSSL_BIN & " ec -pubout -outform DER -in " & sEcdsaKeyFile & " -out " & sEcdsaPublicKeyDerFile
    
        Dim ePublicKeyDerStatus As WshExecStatus, sPublicKeyDerStdOut As String, sPublicKeyDerStdErr As String
        ePublicKeyDerStatus = RunShellAndWait(sPublicKeyDerCmd, sPublicKeyDerStdOut, sPublicKeyDerStdErr)
        Debug.Assert Len(Trim(sPublicKeyDerStdErr))
        Debug.Assert fso.FileExists(sEcdsaPublicKeyDerFile)
    
    
    
        Dim sPublicKeyPemCmd As String
        sPublicKeyPemCmd = sOPENSSL_BIN & " ec -pubout -outform PEM -in " & sEcdsaKeyFile & " -out " & sEcdsaPublicKeyPemFile
    
        Dim ePublicKeyPemStatus As WshExecStatus, sPublicKeyPemStdOut As String, sPublicKeyPemStdErr As String
        ePublicKeyPemStatus = RunShellAndWait(sPublicKeyPemCmd, sPublicKeyPemStdOut, sPublicKeyPemStdErr)
        Debug.Assert Len(Trim(sPublicKeyPemStdErr))
        Debug.Assert fso.FileExists(sEcdsaPublicKeyPemFile)
        
        If ePublicKeyPemStatus = WshFailed Then
            Debug.Print sPublicKeyPemStdErr
        Else
            Debug.Print sPublicKeyPemStdOut
            Debug.Assert fso.FileExists(sEcdsaPublicKeyDerFile)
            Debug.Assert fso.FileExists(sEcdsaPublicKeyPemFile)
            OpenSSL_GenECDSAKeys = True
        End If
    End If

End Function

Private Function OpenSSL_Hash(ByVal sEcdsaKeyFile As String, _
                                    ByVal sFileToSign As String, _
                                    ByVal sHashFile As String) As Boolean
    '* this generates a hash file, I needed this because I'm trying to diagnose
    '* a problem with some C# code which also hashes some bytes
                                    
    Debug.Assert fso.FileExists(sFileToSign)
    Debug.Assert fso.FileExists(sEcdsaKeyFile)
    Debug.Assert fso.FileExists(sOPENSSL_BIN)

    If fso.FileExists(sHashFile) Then fso.DeleteFile sHashFile
    Debug.Assert Not fso.FileExists(sHashFile)

    Dim sHashCmd As String
    sHashCmd = sOPENSSL_BIN & " dgst -sha256 -out " & sHashFile & " " & sFileToSign
    Dim eHashStatus As WshExecStatus, sHashStdOut As String, sHashStdErr As String
    eHashStatus = RunShellAndWait(sHashCmd, sHashStdOut, sHashStdErr)
    
    If eHashStatus = WshFailed Then
        Debug.Print sHashStdErr
    Else
    
        Debug.Print sHashStdOut
        Debug.Assert fso.FileExists(sHashFile)
        
        Debug.Print fso.OpenTextFile(sHashFile).ReadAll
        
        OpenSSL_Hash = True
        
    End If

End Function


Private Function OpenSSL_SignDigest(ByVal sEcdsaKeyFile As String, _
                                    ByVal sFileToSign As String, _
                                    ByVal sSignatureFile As String, _
                                    ByVal sSignatureFileBase64 As String) As Boolean

    '* this code takes a signable file, hashes it to a digest and then signs the digest
    '* further, it creates a base 64 version of the signature file the contents
    '* of which can be cut and pasted into an email.

    Debug.Assert fso.FileExists(sFileToSign)
    Debug.Assert fso.FileExists(sEcdsaKeyFile)
    Debug.Assert fso.FileExists(sOPENSSL_BIN)

    If fso.FileExists(sSignatureFile) Then fso.DeleteFile sSignatureFile
    Debug.Assert Not fso.FileExists(sSignatureFile)

    Dim sSignCmd As String
    sSignCmd = sOPENSSL_BIN & " dgst -sha256 -sign " & sEcdsaKeyFile & " -out " & sSignatureFile & " " & sFileToSign
    Dim eSignStatus As WshExecStatus, sSignStdOut As String, sSignStdErr As String
    eSignStatus = RunShellAndWait(sSignCmd, sSignStdOut, sSignStdErr)
    
    If eSignStatus = WshFailed Then
        Debug.Print sSignStdErr
    Else
        Debug.Print sSignStdOut
        Debug.Assert fso.FileExists(sSignatureFile)
        'OpenSSL_SignDigest = True
        
        Dim sEditedSignatureFile As String
        sEditedSignatureFile = VBA.Replace(sSignatureFile, ".sig", ".siged")
        
        ExtractSignatureFromDerFile sSignatureFile, sEditedSignatureFile
        Debug.Assert fso.FileExists(sEditedSignatureFile)
        
        Dim sBase64Cmd As String
        sBase64Cmd = sOPENSSL_BIN & " base64 -in " & sEditedSignatureFile & " -out " & sSignatureFileBase64

        Dim eBase64Status As WshExecStatus, sBase64StdOut As String, sBase64StdErr As String
        eBase64Status = RunShellAndWait(sBase64Cmd, sBase64StdOut, sBase64StdErr)

        If eBase64Status = WshFailed Then
            Debug.Print sBase64StdErr
        Else
            Debug.Print sBase64StdOut
            Debug.Assert fso.FileExists(sSignatureFileBase64)

            OpenSSL_SignDigest = True
        End If
        
    End If

End Function



Private Function OpenSSL_VerifySignature(ByVal sEcdsaKeyFile As String, _
                                        ByVal sSignatureFile As String, _
                                        ByVal sFileToSign As String) As Boolean
    '* this was for debugging only because I'm trying to diagnose a C#
    '* problem and was wondering if I was calling the OpenSSL commands wrong.
                                        
    Debug.Assert fso.FileExists(sSignatureFile)
    Debug.Assert fso.FileExists(sEcdsaKeyFile)
    Debug.Assert fso.FileExists(sFileToSign)
    
    
    Dim sVerifyCmd As String
    sVerifyCmd = sOPENSSL_BIN & " dgst -sha256 -verify " & sEcdsaKeyFile & " -signature " & sSignatureFile & " " & sFileToSign
    
    Dim eVerifyStatus As WshExecStatus, sVerifyStdOut As String, sVerifyStdErr As String
    eVerifyStatus = RunShellAndWait(sVerifyCmd, sVerifyStdOut, sVerifyStdErr)
    
    If StrComp(Left$(sVerifyStdOut, 11), "Verified OK") = 0 Then
        Debug.Print "Verification success"
    Else
        Debug.Print "Verification failed"
        Debug.Print "err:" & sVerifyStdErr
    End If
    
    OpenSSL_VerifySignature = True
    
End Function





Private Function RunShellAndWait(ByVal sCmdLine As String, _
                                ByRef psStdOut As String, _
                                ByRef psStdErr As String) As IWshRuntimeLibrary.WshExecStatus
    
    '* This is a better Shell routine than the in built VBA.Shell function as it allows up
    '* to easily capture the standard out and standard error pipes which is essential
    '* for console commands
    
    Static oShell As IWshRuntimeLibrary.WshShell
    If oShell Is Nothing Then
        Set oShell = New IWshRuntimeLibrary.WshShell
    End If
    
    Debug.Print sCmdLine
    Dim oExec As IWshRuntimeLibrary.WshExec
    Set oExec = oShell.Exec(sCmdLine)

    While oExec.Status = WshRunning
        DoEvents
    Wend
    
    psStdOut = oExec.StdOut.ReadAll
    psStdErr = oExec.StdErr.ReadAll

    RunShellAndWait = oExec.Status
    Set oExec = Nothing
End Function


Private Function RegExpOffsetAndLengthFromASN1Parse(ByVal sLine As String, ByRef plOffset As Long, ByRef plLength As Long) As Boolean

    '* use regular expressions to dig out numbers from ASN1 parse results
    '* if always using 384 bit then we always be 20 and 98
    '* but for future use we may use 512 bit or a change of curve etc.

    Dim oRE As VBScript_RegExp_55.RegExp
    Set oRE = New VBScript_RegExp_55.RegExp
    oRE.Pattern = "(\d.):d=1  hl=2 l=  (\d.) prim: BIT STRING"
    Debug.Assert oRE.Test("20:d=1  hl=2 l=  98 prim: BIT STRING")  '* should always be this but for future use ....
    
    Dim oMatchCol As VBScript_RegExp_55.MatchCollection
    Set oMatchCol = oRE.Execute(sLine)
    
    Dim oMatch As VBScript_RegExp_55.Match
    Set oMatch = oMatchCol.Item(0)
    
    plOffset = oMatch.SubMatches(0)
    plLength = oMatch.SubMatches(1)
    
    RegExpOffsetAndLengthFromASN1Parse = True
End Function


Private Sub TestExtractSignatureFromDerFile()
    Dim sSigFile As String
    sSigFile = "N:\ecdsa\2017-11-05T225657\license.sig"
    Debug.Assert fso.FileExists(sSigFile)
    
    Dim sSigFileEd As String
    sSigFileEd = "N:\ecdsa\2017-11-05T225657\license.siged"
    
    ExtractSignatureFromDerFile sSigFile, sSigFileEd

End Sub

Private Function ExtractSignatureFromDerFile(ByVal sSignatureFile As String, ByVal sEditedSignatureFile As String)

    Dim sASN1ParseSigCmd As String
    sASN1ParseSigCmd = sOPENSSL_BIN & " asn1parse -inform DER -in " & sSignatureFile

    Dim eASN1ParseSigStatus As WshExecStatus, sASN1ParseSigStdOut As String, sASN1ParseSigStdErr As String
    eASN1ParseSigStatus = RunShellAndWait(sASN1ParseSigCmd, sASN1ParseSigStdOut, sASN1ParseSigStdErr)
    Debug.Print sASN1ParseSigStdOut


    Dim vOutputSplit As Variant
    vOutputSplit = VBA.Split(sASN1ParseSigStdOut, vbNewLine)
    
    '* remove the traling blank line
    If Trim(vOutputSplit(UBound(vOutputSplit))) = "" Then ReDim Preserve vOutputSplit(0 To UBound(vOutputSplit) - 1)



    Dim lIntegerLoop As Long: lIntegerLoop = 0

    Dim alOffsets(0 To 1) As Long
    Dim alLengths(0 To 1) As Long
    Dim asIntegers(0 To 1) As String
    

    Dim lTotalBytes As Long: lTotalBytes = 0
    Dim vLoop As Variant
    For Each vLoop In vOutputSplit
        Dim sSigInt As String: sSigInt = vbNullString
        Dim lOffset As Long: lOffset = 0
        Dim lLength As Long: lLength = 0
        If RegExpSignatureInteger(vLoop, lOffset, lLength, sSigInt) Then
            
            alOffsets(lIntegerLoop) = lOffset
            alLengths(lIntegerLoop) = lLength
            asIntegers(lIntegerLoop) = sSigInt
            
            lTotalBytes = lTotalBytes + lLength
            
            lIntegerLoop = lIntegerLoop + 1
        End If
    Next vLoop
    
    Dim abytSig() As Byte
    Dim asSigHexs() As String  '* for debugging

    '* read in the whole file into a byte array
    ReadFileAsBytes sSignatureFile, abytSig
    
    '* for debugging create an array of hexadecimals
    ByteArrayToHexStringArray abytSig, asSigHexs
    
    For lIntegerLoop = 0 To 1
        fso.CreateTextFile fso.BuildPath(BatchDir(), lIntegerLoop & "_" & alLengths(lIntegerLoop))
        If alLengths(lIntegerLoop) = 49 Then
            
            '* silly null byte
            'Debug.Assert abytSig(3) = 0
            
            '* ignore the null byte
            alLengths(lIntegerLoop) = 48
            alOffsets(lIntegerLoop) = alOffsets(lIntegerLoop) + 1
        
        End If
    Next
    
    Dim abytInteger0() As Byte
    CopyArraySlice abytSig, alOffsets(0) + 2, alLengths(0), abytInteger0()
    
    Dim abytInteger1() As Byte
    CopyArraySlice abytSig, alOffsets(1) + 2, alLengths(1), abytInteger1()
    
    
    ReDim abytIntegers0And1(0 To alLengths(0) + alLengths(1) - 1) As Byte
    
    
    Dim lConcatByteLoop As Long: lConcatByteLoop = 0
    lIntegerLoop = 0
    For lIntegerLoop = 0 To 1
        Dim lByteLoop As Long
        For lByteLoop = 0 To alLengths(lIntegerLoop) - 1
            Dim byt As Byte
            If lIntegerLoop = 0 Then
                byt = abytInteger0(lByteLoop)
            Else
                byt = abytInteger1(lByteLoop)
            End If
        
            abytIntegers0And1(lConcatByteLoop) = byt
            lConcatByteLoop = lConcatByteLoop + 1
        Next lByteLoop
    
    Next lIntegerLoop
    
    'Dim lByteLoop As Long
    'for lByteLoop-
    
    'Stop
    
'    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
'
    
    
    
    'Stop
    
    'Debug.Assert dicIntegers.Count = 2
    'sIntegers(0) = dicIntegers(dicIntegers.Keys()(0))
    'sIntegers(1) = dicIntegers(dicIntegers.Keys()(1))
    
    'Debug.Assert Len(sIntegers(0)) = Len(sIntegers(1))
    'Debug.Assert Len(sIntegers(0)) Mod 2 = 0
    
'    Dim lStringLen As Long
'    lStringLen = Len(sIntegers(0))
'
'    Dim lByteLen As Long
'    lByteLen = lStringLen / 2
'
'
'    ReDim byt(0 To lTotalBytes - 1) As Byte
'
'    Dim lIntegerLoop As Long
'    For lIntegerLoop = 0 To 1
'        Dim lCharLoop As Long
'        For lCharLoop = 0 To lByteLen - 1
'
'            Dim sChar2 As String
'            sChar2 = Mid$(sIntegers(lIntegerLoop), (lCharLoop * 2) + 1, 2)
'
'            Debug.Assert Len(sChar2) > 0
'
'            Dim bChar2 As Byte
'            bChar2 = Val("&h" & sChar2)
'
'            byt(lCharLoop + lIntegerLoop * lByteLen) = bChar2
'
'        Next lCharLoop
'    Next
    
    If fso.FileExists(sEditedSignatureFile) Then fso.DeleteFile sEditedSignatureFile
    SaveBytesAsFile sEditedSignatureFile, abytIntegers0And1()
    
    Debug.Assert fso.FileExists(sEditedSignatureFile)
    

End Function


Private Function RegExpSignatureInteger(ByVal sLine As String, ByRef plOffset As Long, ByRef plLength As Long, ByRef psSignatureInterger As String) As Boolean
    

    '* use regular expressions to dig out numbers from ASN1 parse results
    '* if always using 384 bit then we always be 20 and 98
    '* but for future use we may use 512 bit or a change of curve etc.

    Dim oRE As VBScript_RegExp_55.RegExp
    Set oRE = New VBScript_RegExp_55.RegExp
    oRE.Pattern = "^\s*(\d+):d=1  hl=2 l=  (\d+) prim: INTEGER\s*:(.+)"
    
    
    Debug.Assert oRE.Test("    2:d=1  hl=2 l=  49 prim: INTEGER           :ADFF05570D228E657E98F5455D1772F23224389641F9FA8DCC37B39816BE0C9F871BBABAF4F73ECDDB7BA6814502A85D")
    Debug.Assert oRE.Test("   53:d=1  hl=2 l=  49 prim: INTEGER           :83A977122AF40623AF0C1C64CDEA761E77C534FCA267F2DA6F340CE6AB371F8419464DCC38C5F41729EB421F384A72A1")
    Debug.Assert oRE.Test("    2:d=1  hl=2 l=  48 prim: INTEGER           :195848488FC6A5888D6433E5B714A45A515EE6CBF1B0C952EE35E51F6D678A2623170AE9DE2E0A29B7D47EDC8F95C264")
    Debug.Assert oRE.Test("   52:d=1  hl=2 l=  48 prim: INTEGER           :4789B87BFFC7AC24DA6A9223805EC25EDD6444B483689B2CDBA73056F939C0127228F2632A387BDF6AF93B9A3EAC548E")
    
    If oRE.Test(sLine) Then
    
        Dim oMatchCol As VBScript_RegExp_55.MatchCollection
        Set oMatchCol = oRE.Execute(sLine)
        
        Dim oMatch As VBScript_RegExp_55.Match
        Set oMatch = oMatchCol.Item(0)
        
        plOffset = oMatch.SubMatches(0)
        plLength = oMatch.SubMatches(1)
        psSignatureInterger = oMatch.SubMatches(2)
        
        
        RegExpSignatureInteger = True
    End If
End Function


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
    
    '
    '  
    '    
    '  
    '  
    '    
    '    
    '  
    '


    Dim sASN1ParseCmd As String
    sASN1ParseCmd = sOPENSSL_BIN & " asn1parse -inform DER -in " & sPublicKeyFile

    Dim eASN1ParseStatus As WshExecStatus, sASN1ParseStdOut As String, sASN1ParseStdErr As String
    eASN1ParseStatus = RunShellAndWait(sASN1ParseCmd, sASN1ParseStdOut, sASN1ParseStdErr)
    Debug.Print sASN1ParseStdOut
    
    '* 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(sASN1ParseStdOut, 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
    ReadFileAsBytes 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

Private Function CopyArraySlice(ByRef abOriginal() As Byte, _
            ByVal lStart As Long, ByVal lLength As Long, ByRef abNewSlice() As Byte) As Boolean
            
    '* this copies a slice of an array of bytes to another
    '* caller declares the result array and this code re-dimensions it
            
    Debug.Assert lLength >= 1
    Debug.Assert lStart >= 0
    ReDim abNewSlice(0 To lLength - 1)

    Dim lLoop As Long
    For lLoop = 0 To lLength - 1
        Dim lLoop2 As Long
        lLoop2 = lLoop + lStart
    
        abNewSlice(lLoop) = abOriginal(lLoop2)
    Next lLoop
End Function


Private Function ReadFileAsBytes(ByVal sFile As String, ByRef abytes() As Byte)

    '* ActiveX Data Objects has a binary load to byte array feature ....

    Dim strm As ADODB.Stream
    Set strm = New ADODB.Stream
    With strm
        .Open
        .Type = adTypeBinary
        .LoadFromFile sFile
        abytes = .Read
        .Close
    End With
    Set strm = Nothing

End Function

Private Function SaveBytesAsFile(ByVal sFile As String, ByRef abytes() As Byte)

    Dim lFile As Long
    lFile = FreeFile
    
    Open sFile For Binary Access Write Lock Write As #lFile
    Put #lFile, , abytes()
    Close #lFile

'
'    Dim strm As ADODB.Stream
'    Set strm = New ADODB.Stream
'    With strm
'        .Open
'        .Type = adTypeBinary
'        .Write abytes
'        .SaveToFile sFile
'        '.LoadFromFile sFile
'        'abytes = .Read
'        .Close
'    End With
'    Set strm = Nothing
'
End Function


Private Function ByteArrayToHexStringArray(ByRef abytes() As Byte, ByRef asHexs() As String)
    '* used for debugging

    ReDim asHexs(LBound(abytes) To UBound(abytes))
    
    Dim lLoop As Long
    For lLoop = LBound(abytes) To UBound(abytes)
        asHexs(lLoop) = Hex$(abytes(lLoop))

    Next lLoop
End Function


Private Function ByteArrayToHexString(ByRef ab() As Byte, Optional sDelimiter As String = " ") As String
    '* used to create very long hex numbers as strings
    '* cryptography has long numbers for keys
    Dim sSegment As String
    sSegment = ""
    
    Dim lLoop As Long
    For lLoop = 0 To UBound(ab)
        sSegment = sSegment & sDelimiter & Right$("0" & Hex$(ab(lLoop)), 2)
    Next
    
    ByteArrayToHexString = Trim(sSegment)

End Function

Private Function HexToDecimal(ByVal sHex As String) As String
    '* takes a long hexadecimal number (as a string) and converts
    '* to a long decimal number (as a string)
    
    '* translated from StackOverflow answer
    '* https://stackoverflow.com/questions/16965915/convert-a-big-hex-number-string-format-to-a-decimal-number-string-format-w/46817799
    
    '* tested against websites such as
    '* http://www.statman.info/conversions/hexadecimal.php
    
    Dim dec() As Long
    ReDim dec(0 To 0) As Long
    
    Dim lCharLoop As Long
    For lCharLoop = 1 To Len(sHex)
        
        Dim char As String * 1
        char = Mid$(sHex, lCharLoop, 1)
        
        Dim carry As Long
        carry = Val("&h" & char)
        
        Dim i As Long
        For i = 0 To UBound(dec)
            Dim lVal As Long
            lVal = dec(i) * 16 + carry
            dec(i) = lVal Mod 10
            carry = lVal \ 10
        Next i
    
        While (carry > 0)
            ReDim Preserve dec(0 To UBound(dec) + 1) As Long
            dec(UBound(dec)) = carry Mod 10
            carry = carry \ 10
        Wend
    Next
    
    For lCharLoop = UBound(dec) To LBound(dec) Step -1
        Dim sDecimal As String
        sDecimal = sDecimal & Chr$(48 + dec(lCharLoop))
    
    Next
    
    HexToDecimal = sDecimal

End Function

Private Function ECDSAXml(ByVal sXValue As String, ByVal sYValue As String, Optional ByVal sCurveOid As String = "1.3.132.0.34") As String

    '* sample output
    
    '
    '  
    '    
    '  
    '  
    '    
    '    
    '  
    '

    Dim dom As MSXML2.DOMDocument60
    Set dom = New MSXML2.DOMDocument60
    dom.LoadXML "" & _
                "" & _
                "" & _
                "" & _
                "" & _
                "" & _
                ""
    Debug.Assert dom.parseError.ErrorCode = 0

    '* namespace selection glitch requires us to declare namespace prefix
    '* to use XPath even though original document does not
    dom.setProperty "SelectionNamespaces", "xmlns:ec=""http://www.w3.org/2001/04/xmldsig-more#"""
    
    
    Dim attrCurveId As MSXML2.IXMLDOMAttribute
    Set attrCurveId = dom.SelectSingleNode("ec:ECDSAKeyValue/ec:DomainParameters/ec:NamedCurve/@URN")
    Debug.Assert attrCurveId.Text = "urn:oid:" & sCurveOid  '  secp384r1 – {1.3.132.0.34}
    
    Dim attrXValue As MSXML2.IXMLDOMAttribute
    Set attrXValue = dom.SelectSingleNode("ec:ECDSAKeyValue/ec:PublicKey/ec:X/@Value")
    Debug.Assert Not attrXValue Is Nothing
    attrXValue.Text = sXValue
    
    Dim attrYValue As MSXML2.IXMLDOMAttribute
    Set attrYValue = dom.SelectSingleNode("ec:ECDSAKeyValue/ec:PublicKey/ec:Y/@Value")
    Debug.Assert Not attrYValue Is Nothing
    attrYValue.Text = sYValue
    
    
    ECDSAXml = PrettyPrintXml(dom)
    'ECDSAXml = dom.XML
End Function

Private Function PrettyPrintXml(ByVal dom As MSXML2.DOMDocument60) As String
    '* this routine inserts line breaks between elements

    Dim reader As MSXML2.SAXXMLReader60
    Set reader = New MSXML2.SAXXMLReader60
    
    Dim writer As MSXML2.MXXMLWriter60
    Set writer = New MSXML2.MXXMLWriter60
    writer.omitXMLDeclaration = True
    writer.indent = True
    
    Set reader.contentHandler = writer
    reader.Parse dom.XML
   
    PrettyPrintXml = writer.output
End Function


Private Function WriteStringToFile(ByVal sContents As String, ByVal sFileName As String) As Boolean
    '* this ought to be a one-liner but with so much other code
    '* I felt it necessary to break out so I can debug and check what is happening
    
    If fso.FileExists(sFileName) Then fso.DeleteFile sFileName
    
    Dim txtSignableContent As Scripting.TextStream
    Set txtSignableContent = fso.CreateTextFile(sFileName)
    txtSignableContent.Write sContents
    txtSignableContent.Close
    Set txtSignableContent = Nothing
    
    Debug.Assert fso.FileExists(sFileName)

    WriteStringToFile = True

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

    
        
    
    
        
        
    





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.IO;
using System.Security.Cryptography;
using System.Xml;

namespace ECDSAVerSig
{
    class Program
    {
        static Action 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
            if (args.Length == 0)
            {
                parentDirectory = "n:\\ECDSA\\2017-11-06T212507\\";
            }
            else
            {
                parentDirectory = args[0];
            }
            feedback = Console.WriteLine; // Abstract away 


            if (Directory.Exists(parentDirectory))
            {


                feedback("Processing directory " + parentDirectory);

                if (LoadSignature())
                {
                    VerifySignature();
                }
            }
            else
            {
                feedback("#ERROR: directory '" + parentDirectory + "' does not exist!");
            }


        }



        static private Boolean VerifySignature()
        {
            try
            {
                // a byte array to store hash value
                byte[] hashedData = null;

                hashedData = hashAlgo.ComputeHash(fileContents);

                //* 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!");
                    System.IO.File.Create(Path.Combine(parentDirectory, "win.txt"));
                }
                else
                {
                    feedback("Oh dear, still does not work.  Please keep twiddling.");
                    System.IO.File.Create(Path.Combine(parentDirectory, "lose.txt"));
                }

                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);


                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 do not know what next to do. Please assist. Thanks in advance