Search code examples
openssldllimportnim-lang

How do you import different proc from openssl depending on the openssl version in nim?


The Goal

I wanted to have a PBKDF2-HMAC-SHA256 hashing proc in nim and use the implementation provided by openssl for that. This shall work with both openssl 1 and openssl 3.

The Problem

As part of that process, I need to figure out the correct size the hash should have for the given hashing function. OpenSSL 3 provides those with a function called EVP_MD_get_size.

If you only deal with openSSL3, you can just use:

from std/openssl import DLLSSLName, EVP_MD, EVP_sha256, DLLUtilName

proc EVP_MD_size_fixed*(md: EVP_MD): cint {.cdecl, dynlib: DLLUtilName, importc: "EVP_MD_get_size".}

That'll give you EVP_MD_get_size renamed to a proc called EVP_MD_size_fixed.

But if the openssl version is 1 and not 3 (e.g. because of debian), then the same proc is called "EVP_MD_size", not EVP_MD_get_size. So you can't import it. You also can't just blindly import both EVP_MD_size and EVP_MD_get_size because it will throw a runtime error:

import std/[dynlib, openssl, strformat]

proc EVP_MD_size_fixed1*(md: EVP_MD): cint {.cdecl, dynlib: DLLUtilName, importc: "EVP_MD_get_size".}
proc EVP_MD_size_fixed2*(md: EVP_MD): cint {.cdecl, dynlib: DLLUtilName, importc: "EVP_MD_size".}

proc getOpenSSLMajorVersion(): uint =(getOpenSSLVersion() shr 28) and 0xF # Returns the major version of openssl

proc EVP_MD_size_fixed*(md: EVP_MD): cint =
  assert md != nil
  result =
    if getOpenSSLMajorVersion() == 3:
      EVP_MD_size_fixed1(md)
    elif getOpenSSLMajorVersion() == 1:
      EVP_MD_size_fixed2(md)
    else:
      raise newException(ValueError, fmt"This library supports only openssl 1 and 3. The openssl version we found was {getOpenSSLMajorVersion()}")


proc EVP_sha256_fixed(): EVP_MD    {.cdecl, dynlib: DLLUtilName, importc: "EVP_sha256".}
let digestFunction = EVP_sha256_fixed()

let hashLength: cint = EVP_MD_size_fixed(digestFunction)
echo hashLength

On a system with openssl3 leads to: could not import: EVP_MD_size

Now how do I make it so I call EVP_MD_size when the available openssl major version is 1 and EVP_MD_get_size when the available openssl major version is 3?


Solution

  • The answer as provided by ElegantBeef and Yakoleb from the nim discord server is std/dynlib. It allows loading a library at runtime, loading procs from it and calling them.

    The Code:

    import std/[dynlib, openssl, strformat]
    
    type DigestSizeProc = proc(md: EVP_MD): cint {.cdecl.}
    
    proc getOpenSSLMajorVersion(): uint =(getOpenSSLVersion() shr 28) and 0xF # Returns the major version of openssl
    
    # Load lib
    let lib = loadLibPattern(DLLUtilName)
    assert lib != nil, fmt"Could not find lib {DLLUtilName}"
    
    # Load proc <-- HERE IS THE MAGIC
    let evp_md_size_getter: DigestSizeProc = 
      if getOpenSSLMajorVersion() == 3:
        cast[DigestSizeProc](lib.symAddr("EVP_MD_get_size"))
      elif getOpenSSLMajorVersion() == 1:
        cast[DigestSizeProc](lib.symAddr("EVP_MD_size"))
      else:
        raise newException(ValueError, fmt"This library supports only openssl 1 and 3. The openssl version we found was {getOpenSSLMajorVersion()}")
    assert evp_md_size_fixed != nil, fmt"Failed to load EVP_MD_size or EVP_MD_get_size proc from openssl version {getOpenSSLMajorVersion()}"
    
    unloadLib(lib)
    
    # Use proc
    proc EVP_MD_size_fixed*(md: EVP_MD): cint =
      assert md != nil
      result = evp_md_size_getter(md)
    
    proc EVP_sha256_fixed(): EVP_MD    {.cdecl, dynlib: DLLUtilName, importc: "EVP_sha256".}
    let digestFunction: EVP_MD = EVP_sha256_fixed()
    
    let hashLength: cint = EVP_MD_size_fixed(digestFunction)
    echo hashLength
    

    Explanation:

    Loading the library

    As per the example there from std/dynlib, you can load a library using loadLib. In our case, we're using std/openssl.DLLUtilName as defined in the source-code there, so that'll return you a pattern of libcrypto.so(.3|.1.1|.1.0.2|.1.0.1|.1.0.0|.0.9.9|.0.9.8|.48|.47|.46|.45|.44|.43|.41|.39|.38|.10|). That one requires loadLibPattern though, as this is not just a name of single library but a library-pattern.

    Loading a proc from the library

    We first need to know which name the proc has and which type that proc is. In this case, the name is either EVP_MD_size or EVP_MD_get_size. With that, we can use lib.symAddr("<Symbol string>") to get a pointer to the function. To be able to call it, we need to have nim interpret that pointer as a callable proc. So we'll have to cast it, but to which type?

    We know the general type of that pointer, it's proc(md: EVP_MD): cint (As you can see from the openssl docs where its defined int EVP_MD_get_size(const EVP_MD *md);

    However, we also need to tell nim how to call the proc from openssl, the calling-convention . The one to use here is cdecl, so the final type to cast that pointer to is type DigestSizeProc = proc(md: EVP_MD): cint {.cdecl.}

    Once we know all of that, we can just do a variable assignment as in the Load proc section. Make sure to add asserts behind all the loading of libs/pointers to make sure you can easily figure out where Nil-pointer-access may occur.