Search code examples
rubypointerswinapix509certificatecryptoapi

Unable to create a valid CRYPT_HASH_BLOB in ruby


I'm using Crypto API in Ruby to find a windows certificate using it's Thumbprint. For that I'm using CertFindCertificateInStore function which requires a CRYPT_HASH_BLOB structure.

I'm dealing with pointers by using FFI::MemoryPointer. Have tried various approaches to create this struct but none of them seems to be working.

Can someone please take a look and provide me suggestions.

Here is the complete code for the reference:

    require "ffi"

    module LibC
      extend FFI::Library
      ffi_lib FFI::Library::LIBC

      # memory allocators
      attach_function :malloc, [:size_t], :pointer
      attach_function :calloc, [:size_t], :pointer
      attach_function :free, [:pointer], :void
    end

    module Crypto
      extend LibC
      extend FFI::Library
      ffi_lib "Crypt32"

      HCERTSTORE               = FFI::TypeDefs[:pointer]
      HCRYPTPROV_LEGACY        = FFI::TypeDefs[:pointer]
      PCCERT_CONTEXT           = FFI::TypeDefs[:pointer]
      DWORD                    = FFI::TypeDefs[:uint32]
      BLOB                     = FFI::TypeDefs[:ulong]
      LPCTSTR                  = FFI::TypeDefs[:pointer]
      BOOL                     = FFI::TypeDefs[:bool]
      LPVOID                   = FFI::TypeDefs[:pointer]

      class CRYPT_HASH_BLOB < FFI::Struct
        layout :cbData, DWORD, # Count, in bytes, of data
               :pbData, :pointer # Pointer to data buffer

        def initialize(str)
          super(nil)
          if str
            # Method 1: Simply using thumbprint string
            buffer1 = LibC.malloc str.size
            buffer1.write_string str
            self[:pbData] = buffer1
            self[:cbData] = str.size

            # Converting thumbpring string into a byte array
            # arr = [str].pack('H*').unpack('C*')

            # Method 2: Using Byte Array  with LibC
            # buffer2 = LibC.malloc(arr.first.size * arr.size) # Create the pointer to the array
            # buffer2.write_array_of_uint32 arr                # Fill the memory location with data
            # self[:pbData] = buffer2
            # self[:cbData] = arr.size

            # # Method 3: Using Byte Array with FFI::MemoryPointer
            # buffer3 = FFI::MemoryPointer.new :uint32, arr.size # Create the pointer to the array
            # buffer3.put_array_of_uint32 0, arr                 # Fill the memory location with data
            # self[:pbData] = buffer3
            # self[:cbData] = arr.size
          end
        end
      end

      attach_function :CertOpenStore, [DWORD, DWORD, HCRYPTPROV_LEGACY, DWORD, LPCTSTR], HCERTSTORE
      attach_function :CertCloseStore, [HCERTSTORE, DWORD], BOOL
      attach_function :CertFindCertificateInStore, [HCERTSTORE, DWORD, DWORD, DWORD, LPVOID, PCCERT_CONTEXT], PCCERT_CONTEXT
      attach_function :CertFreeCertificateContext, [PCCERT_CONTEXT], BOOL
    =begin
      PCCERT_CONTEXT CertFindCertificateInStore(
        HCERTSTORE     hCertStore,
        DWORD          dwCertEncodingType,
        DWORD          dwFindFlags,
        DWORD          dwFindType,
        const void     *pvFindPara,
        PCCERT_CONTEXT pPrevCertContext
      );
    =end
    end

    class CertificateHandler
      include Crypto
      CERT_STORE_PROV_SYSTEM = 10
      CERT_SYSTEM_STORE_LOCAL_MACHINE = 0x00020000
      X509_ASN_ENCODING = 0x00000001
      PKCS_7_ASN_ENCODING = 0x00010000
      ENCODING_TYPE = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING
      CERT_CLOSE_STORE_FORCE_FLAG = 1

      CERT_COMPARE_SHA1_HASH = 1
      CERT_COMPARE_SHIFT = 16
      CERT_FIND_SHA1_HASH = CERT_COMPARE_SHA1_HASH << CERT_COMPARE_SHIFT

      def self.finalize(certstore_handler)
        proc { certstore_handler.to_s }
      end

      def add_finalizer(certstore_handler)
        ObjectSpace.define_finalizer(self, self.class.finalize(certstore_handler))
      end

      def remove_finalizer
        ObjectSpace.undefine_finalizer(self)
      end

      def utf8_to_wide(ustring)
        # ensure it is actually UTF-8
        # Ruby likes to mark binary data as ASCII-8BIT
        ustring = (ustring + "").force_encoding("UTF-8") if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8"

        # ensure we have the double-null termination Windows Wide likes
        ustring += "\000\000" if ustring.length == 0 || ustring[-1].chr != "\000"

        # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode
        ustring = ustring.encode("UTF-16LE") if ustring.respond_to?(:encode)
        ustring
      end

      def cert_find_by_thumbprint(thumbprint = nil)
        store_name = "Root"
        thumbprint ||= "1D F4 AB B6 13 F2 12 27 1C 04 F8 52 9D DE 38 E4 B7 24 2E 6C" # Assume this is a valid thumbprint in Root
        thumbprint.gsub!(/[^A-Za-z0-9]/, "") # Discard WhiteSpaces

        certstore_handler = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, nil,
          CERT_SYSTEM_STORE_LOCAL_MACHINE, utf8_to_wide(store_name))
        add_finalizer(certstore_handler)

        pcert_context = CertFindCertificateInStore(certstore_handler, ENCODING_TYPE, 0, CERT_FIND_SHA1_HASH, CRYPT_HASH_BLOB.new(thumbprint), nil)

        puts "Certificate Found = #{!pcert_context.null?}" # It should be true

        CertFreeCertificateContext(pcert_context)
        closed = CertCloseStore(@certstore_handler, CERT_CLOSE_STORE_FORCE_FLAG)
        remove_finalizer
      end
    end

    CertificateHandler.new.cert_find_by_thumbprint

Solution

  • I'm not familiar with Ruby at all, but I can make some assumptions. Correct me if I'm wrong.

    You are converting source thumbprint (SHA1 value) to a string without spaces between octets:

    thumbprint ||= "1D F4 AB B6 13 F2 12 27 1C 04 F8 52 9D DE 38 E4 B7 24 2E 6C" # Assume this is a valid thumbprint in Root
    thumbprint.gsub!(/[^A-Za-z0-9]/, "") # Discard WhiteSpaces
    

    This will result something like this: 1DF4ABB613F212271C04F8529DDE38E4B7242E6C and then you pass this string into CRYPT_HASH_BLOB constructor in this line:

    pcert_context = CertFindCertificateInStore(certstore_handler, ENCODING_TYPE, 0, CERT_FIND_SHA1_HASH, CRYPT_HASH_BLOB.new(thumbprint), nil)
    

    In the CRYPT_HASH_BLOB.initialize(str) you unpack the utf8 string to a byte array. First method is incorrect, because you treat input string as raw char array. Instead, it is a hex-encoded byte array, thus arr = [str].pack('H*').unpack('C*') line must be involved. At least, google suggests that this line will convert a series of octets to a corresponding byte array. So, method 3 is just fine with the exception that you must allocate *byte* array (not integer array) and write chars instead of ints. The correct constructor code should look like this:

    def initialize(str)
        super(nil)
        if str
            # Converting thumbpring string into a byte array
            arr = [str].pack('H*').unpack('C*')
    
            # Method 3: Using Byte Array with FFI::MemoryPointer
            buffer3 = FFI::MemoryPointer.new(:char, arr.size) # Create the pointer to the array
            buffer3.write_array_of_char(arr) # Fill the memory location with data
            self[:pbData] = buffer3
            self[:cbData] = arr.size
        end
    end
    

    I believe, this will do the work.