OpenSSL Engine加载

 ·  ☕ 26   ·  ✍️ Techsum

问题来源

OpenSSL Engine是啥,在这个地方就不细说了,资料很多,可以看看知乎这篇中文文档:

https://zhuanlan.zhihu.com/p/70444766

英文文档:

https://wiki.openssl.org/index.php/Creating_an_OpenSSL_Engine_to_use_indigenous_ECDH_ECDSA_and_HASH_Algorithms#Author

直接进入正题,我们首先查看一个OpenSSL Engine的例子:

https://github.com/nibrunie/OSSL_EngineX

直接查看bind代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int bind(ENGINE* e, const char* id) 
{
  int ret = 0;
  if (!ENGINE_set_id(e, engine_id)) {
    fprintf(stderr, "ENGINE_set_id failed\n");
    goto end;
  }
  if (!ENGINE_set_name(e, engine_name)) {
    printf("ENGINE_set_name failed\n");
    goto end;
  }
  if (!ENGINE_set_digests(e, digest_selector)) {
    printf("ENGINE_set_digest failed\n");
    goto end;
  }

  ret = 1;
end:
  return ret;
}

IMPLEMENT_DYNAMIC_BIND_FN(bind)
IMPLEMENT_DYNAMIC_CHECK_FN()

可以看到OpenSSL去加载Engine的动态库时,需要动态库去调用 IMPLEMENT_DYNAMIC_BIND_FN 完成engine绑定初始化。

基本上所以教你写engine的教程到这就结束了,但是内部到底是怎么要关联上这个函数,并且触发上面的bind函数的呢?我们先来看看这个宏的具体定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
\# define IMPLEMENT_DYNAMIC_BIND_FN(fn) \
        OPENSSL_EXPORT \
        int bind_engine(ENGINE *e, const char *id, const dynamic_fns *fns); \
        OPENSSL_EXPORT \
        int bind_engine(ENGINE *e, const char *id, const dynamic_fns *fns) { \
            if (ENGINE_get_static_state() == fns->static_state) goto skip_cbs; \
            CRYPTO_set_mem_functions(fns->mem_fns.malloc_fn, \
                                     fns->mem_fns.realloc_fn, \
                                     fns->mem_fns.free_fn); \
        skip_cbs: \
            if (!fn(e, id)) return 0; \ /* 调用了上面例子中的bind函数 */
            return 1; }

可以看到此处定义了函数bind_engine,他会去执行用宏包裹住的函数,以完成初始化。然而你去搜索这个函数在OpenSSL中调用你一定会很失望,肯定没有你想要的结果。果然不是这么简单的,又是什么钩子挂在了什么ctx上吧,应该也不难。

我找了不少资料,基本没发现啥靠谱的分析,没办法自己看源码吧。结果经过分析,我深刻的理解了OpenSSL的魔鬼调用,钩子的挂载可以说是很魔幻。此处源码分析基于目前的主线master,应该也是未来OpenSSL 3.0的架构了。

至于Engine是怎么设置上重置后的密码算法的,将在后续更新。

从加载Engine的main函数分析起

还是上面的例子,我们查看执行engine加载的可执行程序的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(void)
{
  // initializing OpenSSL library
  OPENSSL_load_builtin_modules();
  ENGINE_load_dynamic();

  // building OpenSSL's configuration file path
  char openssl_cnf_path[] = "./openssl.cnf"; 

  // loading configuration
  if (CONF_modules_load_file(openssl_cnf_path, "openssl_conf", 0) != 1) {
    fprintf(stderr, "OpenSSL failed to load required configuration\n");
    ERR_print_errors_fp(stderr);
    return 1;
  }

  ENGINE* eng = ENGINE_by_id("engineX");
  if(NULL == eng) {
    printf("failed to retrieve engine by id (mppa)\n");
    return 1;
  }

  printf("EngineX has been successfully loaded \n");
  ...
}

可以看到我们这个例子是从一个cnf配置文件去加载对应的engine的,这里提一句,加载engine有几个方式,如命令行加载,手动代码加载等。这里用配置文件加载做例子是因为这个场景更加接近实际业务场景,而且流程基本涵盖全流程,值得源码去分析。接下来我们按照调用顺序来分析这样一个漫长的调用过程。

OPENSSL_load_builtin_modules

第一个函数,初始化了一个默认的conf_module, 且名字叫做’engines’。直接看源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void OPENSSL_load_builtin_modules(void)
{
    ...
    /* 我们其他的都不重要,直接看这个和Engine相关的 */
#ifndef OPENSSL_NO_ENGINE
    ENGINE_add_conf_module();
#endif
    ...
}

void ENGINE_add_conf_module(void)
{
    CONF_module_add("engines",
                    int_engine_module_init, int_engine_module_finish);
}

来到我们的第一个大坑,OpenSSL的动态配置文件加载,但这里我们不需要细致了解,先简单分析下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
int CONF_module_add(const char *name, conf_init_func *ifunc,
                    conf_finish_func *ffunc)
{
    if (module_add(NULL, name, ifunc, ffunc))
        return 1;
    else
        return 0;
}

/* 重要的结构体与全局变量 */
static STACK_OF(CONF_MODULE) *supported_modules = NULL;
static STACK_OF(CONF_IMODULE) *initialized_modules = NULL;

struct conf_module_st {
    /* DSO of this module or NULL if static */
    DSO *dso;
    /* Name of the module */
    char *name;
    /* Init function */
    conf_init_func *init;
    /* Finish function */
    conf_finish_func *finish;
    /* Number of successfully initialized modules */
    int links;
    void *usr_data;
};

typedef struct conf_module_st CONF_MODULE;


static CONF_MODULE *module_add(DSO *dso, const char *name,
                               conf_init_func *ifunc, conf_finish_func *ffunc)
{
    CONF_MODULE *tmod = NULL;
    /* 若supported_modules为空, 则初始化此全局变量,即堆栈的初始化 */
    if (supported_modules == NULL)
        supported_modules = sk_CONF_MODULE_new_null();
    if (supported_modules == NULL)
        return NULL;
    /* 申请配置文件模块结构体conf_module_st的空间 */
    if ((tmod = OPENSSL_zalloc(sizeof(*tmod))) == NULL) {
        CONFerr(CONF_F_MODULE_ADD, ERR_R_MALLOC_FAILURE);
        return NULL;
    }
    
    /* 
     * 此处第一次调用,dso为NULL; 
     * dso = dynamic shared object, 可以理解为是一个OpenSSL去加载动态库的结构体;
     */
    tmod->dso = dso;
    /* 此处记住,将初始化一个叫"engines"的conf_module */
    tmod->name = OPENSSL_strdup(name);
    /* 配置文件init函数, 此处即int_engine_module_init。这个函数是关键 */
    tmod->init = ifunc;
    /* 配置文件finish函数, 此处即int_engine_module_finish */
    tmod->finish = ffunc;
    if (tmod->name == NULL) {
        OPENSSL_free(tmod);
        return NULL;
    }

    /* 将这个的conf_module结构体入栈进supported_modules这个全局变量栈中 */
    if (!sk_CONF_MODULE_push(supported_modules, tmod)) {
        OPENSSL_free(tmod->name);
        OPENSSL_free(tmod);
        return NULL;
    }

    return tmod;
}

此处有一个OpenSSL的一个知识点,OpenSSL中可以定义任意类型的安全栈,并且生成操作这个类型栈的函数族。例如有一个结构体叫XX,则可以通过DEFINE_STACK_OF(XX)这个宏来定义XX结构体的栈和函数族,通过STACK_OF(XX)来声明一个栈。事实上,当我们看OpenSSL源码时看到sk_这种前缀的都是堆栈操作,而且是搜索不到实现的 (1.0.2版本应该可以找到,之后的版本都泛化了,代码写的秀,看代码的自闭)。

详见官方文档:https://www.openssl.org/docs/man1.1.0/man3/DEFINE_STACK_OF.html

此处有两个栈操作: 初始化时supported_modules为空,所以将调用sk_CONF_MODULE_new_null先建立上一个空容器。之后sk_CONF_MODULE_push使上面初始化的的CONF_MODULE入栈,之后想要取到这个module则需要通过supported_modules这个全局栈来取。

此处多提一句,OpenSSL还有一个类似的结构体LHASH,它是OpenSSL内部的哈希表,如果这篇文章有下我们应该会碰到它,直接理解成是一个kv_map就好。所有lh_前缀开头的也都是哈希表操作。

ENGINE_load_dynamic

第二个函数,比较绕,简单理解就是:初始化了一个engine, 名字叫做dynamic,OpenSSL用这个engine来动态加载别的engine…

顺便提一句,ENGINE_load_dynamic 在1.1.x版本已经废弃了,统一都是调用OPENSSL_init_crypto这个函数,opts = OPENSSL_INIT_ENGINE_DYNAMIC。这又是OpenSSL非常恶心的地方了,版本兼容可以说是相当emmmmmmmm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
\# define ENGINE_load_dynamic() \
    OPENSSL_init_crypto(OPENSSL_INIT_ENGINE_DYNAMIC, NULL)

int OPENSSL_init_crypto(uint64_t opts, const OPENSSL_INIT_SETTINGS *settings)
{
    ...
        /* 
         * RUN_ONCE是多线程时需要关心的,我们这里不关心,就等于调用ossl_init_engine_dynamic 
         * 最后一波宏展开,调用的是 engine_load_dynamic_int 这个函数
         */
        if ((opts & OPENSSL_INIT_ENGINE_DYNAMIC)
            && !RUN_ONCE(&engine_dynamic, ossl_init_engine_dynamic)) 
            return 0;
    ...
}

void engine_load_dynamic_int(void)
{
    ENGINE *toadd = engine_dynamic(); /* 这命名真是绝了Orz */
    if (!toadd)
        return;
    ENGINE_add(toadd);
    /*
     * If the "add" worked, it gets a structural reference. So either way, we
     * release our just-created reference.
     */
    ENGINE_free(toadd);
    /*
     * If the "add" didn't work, it was probably a conflict because it was
     * already added (eg. someone calling ENGINE_load_blah then calling
     * ENGINE_load_builtin_engines() perhaps).
     */
    ERR_clear_error();
}

engine_dynamic

两个核心函数,第一个 engine_dynamic 新建了一个id叫做'dynamic'的engine,挂上了这个engine的具体处理函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static ENGINE *engine_dynamic(void)
{
    /* OpenSSL申请结构体空间经常使用的xx_new */
    ENGINE *ret = ENGINE_new();
    if (ret == NULL)
        return NULL;
    if (!ENGINE_set_id(ret, engine_dynamic_id) ||
        !ENGINE_set_name(ret, engine_dynamic_name) ||
        !ENGINE_set_init_function(ret, dynamic_init) ||
        !ENGINE_set_finish_function(ret, dynamic_finish) ||
        !ENGINE_set_ctrl_function(ret, dynamic_ctrl) ||
        !ENGINE_set_flags(ret, ENGINE_FLAGS_BY_ID_COPY) ||
        !ENGINE_set_cmd_defns(ret, dynamic_cmd_defns)) {
        ENGINE_free(ret);
        return NULL;
    }
    return ret;
}

我们扫一眼ENGINE结构体,首先要有一个概念,ENGINE_set_xx 就是去设置这个结构体的相应字段,所以可以记录一下这个结构体被初始化成啥样了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct engine_st {
    const char *id;
    const char *name;
    const RSA_METHOD *rsa_meth;
    const DSA_METHOD *dsa_meth;
    const DH_METHOD *dh_meth;
    const EC_KEY_METHOD *ec_meth;
    const RAND_METHOD *rand_meth;
    /* Cipher handling is via this callback */
    ENGINE_CIPHERS_PTR ciphers;
    /* Digest handling is via this callback */
    ENGINE_DIGESTS_PTR digests;
    /* Public key handling via this callback */
    ENGINE_PKEY_METHS_PTR pkey_meths;
    /* ASN1 public key handling via this callback */
    ENGINE_PKEY_ASN1_METHS_PTR pkey_asn1_meths;
    ENGINE_GEN_INT_FUNC_PTR destroy;
    ENGINE_GEN_INT_FUNC_PTR init;
    ENGINE_GEN_INT_FUNC_PTR finish;
    ENGINE_CTRL_FUNC_PTR ctrl;
    ENGINE_LOAD_KEY_PTR load_privkey;
    ENGINE_LOAD_KEY_PTR load_pubkey;
    ENGINE_SSL_CLIENT_CERT_PTR load_ssl_client_cert;
    const ENGINE_CMD_DEFN *cmd_defns;
    int flags;
    /* reference count on the structure itself */
    CRYPTO_REF_COUNT struct_ref;
    /*
     * reference count on usability of the engine type. NB: This controls the
     * loading and initialisation of any functionality required by this
     * engine, whereas the previous count is simply to cope with
     * (de)allocation of this structure. Hence, running_ref <= struct_ref at
     * all times.
     */
    int funct_ref;
    /* A place to store per-ENGINE data */
    CRYPTO_EX_DATA ex_data;
    /* Used to maintain the linked-list of engines. */
    struct engine_st *prev;
    struct engine_st *next;
}

整理如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static const char *engine_dynamic_id = "dynamic";
static const char *engine_dynamic_name = "Dynamic engine loading support";
static const ENGINE_CMD_DEFN dynamic_cmd_defns[] = {
    {DYNAMIC_CMD_SO_PATH,
     "SO_PATH",
     "Specifies the path to the new ENGINE shared library",
     ENGINE_CMD_FLAG_STRING},
    {DYNAMIC_CMD_NO_VCHECK,
     "NO_VCHECK",
     "Specifies to continue even if version checking fails (boolean)",
     ENGINE_CMD_FLAG_NUMERIC},
    {DYNAMIC_CMD_ID,
     "ID",
     "Specifies an ENGINE id name for loading",
     ENGINE_CMD_FLAG_STRING},
    {DYNAMIC_CMD_LIST_ADD,
     "LIST_ADD",
     "Whether to add a loaded ENGINE to the internal list (0=no,1=yes,2=mandatory)",
     ENGINE_CMD_FLAG_NUMERIC},
    {DYNAMIC_CMD_DIR_LOAD,
     "DIR_LOAD",
     "Specifies whether to load from 'DIR_ADD' directories (0=no,1=yes,2=mandatory)",
     ENGINE_CMD_FLAG_NUMERIC},
    {DYNAMIC_CMD_DIR_ADD,
     "DIR_ADD",
     "Adds a directory from which ENGINEs can be loaded",
     ENGINE_CMD_FLAG_STRING},
    {DYNAMIC_CMD_LOAD,
     "LOAD",
     "Load up the ENGINE specified by other settings",
     ENGINE_CMD_FLAG_NO_INPUT},
    {0, NULL, NULL, 0}
};   /* 加载动态engine时的命令 */

# define ENGINE_FLAGS_BY_ID_COPY         (int)0x0004

ENGINE dynamic = {.id = engine_dynamic_id,
                  .name = engine_dynamic_name,
                  .init = dynamic_init, /* 空函数,直接return 0 */
                  .finish = dynamic_finish, /* 空函数,直接return 0 */
                  .ctrl = dynamic_ctrl, /* 最重要的函数,后文将分析如何调用到这来 */
                  .flags = ENGINE_FLAGS_BY_ID_COPY
                  .cmd_defns = dynamic_cmd_defns /*定义了dynamic这个engine ctrl下的合法cmd*/
                  .prev = NULL, .next = NULL /* 说明engine都是以双向链表形式管理 */
                 };

完成初始化后,将返回上这个new出来的ENGINE结构体。随后丢到ENGINE_add 里。

ENGINE_add

上面结构体分析其实已经可以看到,所有的engine都将以双向链表形式管理,链表建立简单粗暴,直接定义全局变量一头一尾,添加时就往尾巴加,搜索就从头结点开始搜索:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
static ENGINE *engine_list_head = NULL;
static ENGINE *engine_list_tail = NULL;

/* Add another "ENGINE" type into the list. */
int ENGINE_add(ENGINE *e)
{
    int to_return = 1;
    /* 一些入参检查,omit */
    ...
    /* 全局变量操作时需要加锁以支持多线程 */
    CRYPTO_THREAD_write_lock(global_engine_lock);
    /* 核心函数,将刚刚new出来的dynamic加入全局链表中 */
    if (!engine_list_add(e)) {
        ENGINEerr(ENGINE_F_ENGINE_ADD, ENGINE_R_INTERNAL_LIST_ERROR);
        to_return = 0;
    }
    CRYPTO_THREAD_unlock(global_engine_lock);
    return to_return;
}

static int engine_list_add(ENGINE *e)
{
    int conflict = 0;
    ENGINE *iterator = NULL;

    if (e == NULL) {
        ENGINEerr(ENGINE_F_ENGINE_LIST_ADD, ERR_R_PASSED_NULL_PARAMETER);
        return 0;
    }
    
    /* 从链表头开始迭代 */
    iterator = engine_list_head;
    /* 直接遍历到尾部查看有没有重id的情况,重id直接报错退出 */
    while (iterator && !conflict) {
        conflict = (strcmp(iterator->id, e->id) == 0);
        iterator = iterator->next;
    }
    if (conflict) {
        ENGINEerr(ENGINE_F_ENGINE_LIST_ADD, ENGINE_R_CONFLICTING_ENGINE_ID);
        return 0;
    }
    if (engine_list_head == NULL) {
        /* We are adding to an empty list. */
        if (engine_list_tail) {
            ENGINEerr(ENGINE_F_ENGINE_LIST_ADD, ENGINE_R_INTERNAL_LIST_ERROR);
            return 0;
        }
        /* engine_list为空的话则链表头为新建的engine */
        engine_list_head = e;
        e->prev = NULL;
        /*
         * The first time the list allocates, we should register the cleanup.
         */
        engine_cleanup_add_last(engine_list_cleanup);
    } else {
        /* We are adding to the tail of an existing list. */
        if ((engine_list_tail == NULL) || (engine_list_tail->next != NULL)) {
            ENGINEerr(ENGINE_F_ENGINE_LIST_ADD, ENGINE_R_INTERNAL_LIST_ERROR);
            return 0;
        }
        /* 将新engine加到队尾的后面 */
        engine_list_tail->next = e;
        e->prev = engine_list_tail;
    }
    /*
     * Having the engine in the list assumes a structural reference.
     */
    e->struct_ref++;
    engine_ref_debug(e, 0, 1);
    /* 将队尾指向新engine */
    engine_list_tail = e;
    e->next = NULL;
    return 1;
}

这样,id'dynamic'被加入了全局engine列表当中,被管理起来。

CONF

我们这里对OpenSSL的动态配置conf不需要细致分析,随着代码分析即可。官方文档其实对conf格式讲解的很清楚,可以学习:

https://www.openssl.org/docs/man1.1.1/man5/config.html

Engine Configuration Module这个小节

例子中conf文件

首先我们来看engineX例子中的conf是怎么写的:

openssl_conf            = openssl_def
[openssl_def]
engines = engine_section
[engine_section]
engine_x = engine_x_section
[engine_x_section]
engine_id = engineX
dynamic_path = ${ENV::PWD}/build/engine_ex.so 
default_algorithms = ALL
init = 1

简单学习一下conf之后,我们之后这个配置文件核心的section就是engine_section,其中dynamic_path定义上了该engine共享库的路径。我们看看例子中是如何根据这个配置文件去加载对应的engine的

CONF_modules_load_file

1
2
3
4
5
6
7
8
...  
  char openssl_cnf_path[] = "./openssl.cnf"; 

  // loading configuration
  if (CONF_modules_load_file(openssl_cnf_path, "openssl_conf", 0) != 1) {
    ...
  }
...

CONF_modules_load_file是去加载配置并使能配置的接口,这里我们主要关心如何去根据配置文件去加载动态库,具体怎么完成配置文件解析的流程这里不讨论。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int CONF_modules_load_file(const char *filename,
                           const char *appname, unsigned long flags)
{
    return CONF_modules_load_file_with_libctx(NULL, filename, appname, flags);
}

int CONF_modules_load_file_with_libctx(OPENSSL_CTX *libctx,
                                       const char *filename,
                                       const char *appname, unsigned long flags)
{
    char *file = NULL;
    CONF *conf = NULL;
    int ret = 0;

    conf = NCONF_new_with_libctx(libctx, NULL);
    if (conf == NULL)
        goto err;

    if (filename == NULL) {
        file = CONF_get1_default_config_file();
        if (file == NULL)
            goto err;
    } else {
        file = (char *)filename;
    }

    if (NCONF_load(conf, file, NULL) <= 0) {
        if ((flags & CONF_MFLAGS_IGNORE_MISSING_FILE) &&
            (ERR_GET_REASON(ERR_peek_last_error()) == CONF_R_NO_SUCH_FILE)) {
            ERR_clear_error();
            ret = 1;
        }
        goto err;
    }

    ret = CONF_modules_load(conf, appname, flags);

 err:
    if (filename == NULL)
        OPENSSL_free(file);
    NCONF_free(conf);

    if (flags & CONF_MFLAGS_IGNORE_RETURN_CODES)
        return 1;

    return ret;
}

可以看到这里主要有三步操作NCONF_new_with_libctxNCONF_loadCONF_modules_load,我们一个一个分析。

NCONF_new_with_libctx

这个函数主要是初始化上了一个CONF结构体,同时将这个结构体的METHOD定义成了默认方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* 配置文件的method模板 */
struct conf_method_st {
    const char *name;
    CONF *(*create) (CONF_METHOD *meth);
    int (*init) (CONF *conf);
    int (*destroy) (CONF *conf);
    int (*destroy_data) (CONF *conf);
    int (*load_bio) (CONF *conf, BIO *bp, long *eline);
    int (*dump) (const CONF *conf, BIO *bp);
    int (*is_number) (const CONF *conf, char c);
    int (*to_int) (const CONF *conf, char c);
    int (*load) (CONF *conf, const char *name, long *eline);
};

/* 
 * 所有的 AA = BB 都会按照这个格式保存 
 * 如[openssl_def] engines = engine_section
 * 此时这个底下conf_st的哈希表中将保存上一份
 * {.section = "openssl_def", .name = "engines", value = "engine_section"}
 */
typedef struct {
    char *section;
    char *name;
    char *value;
} CONF_VALUE; 

struct conf_st {
    CONF_METHOD *meth;      /* 动态配置的方法,这里使用default */
    void *meth_data;
    LHASH_OF(CONF_VALUE) *data;    /* 上文有提到的哈希表 */
    unsigned int flag_dollarid:1;
    OPENSSL_CTX *libctx;
};

/*
 * The following section contains the "New CONF" functions.  They are
 * completely centralised around a new CONF structure that may contain
 * basically anything, but at least a method pointer and a table of data.
 * These functions are also written in terms of the bridge functions used by
 * the "CONF classic" functions, for consistency.
   */

CONF *NCONF_new_with_libctx(OPENSSL_CTX *libctx, CONF_METHOD *meth)
{
    CONF *ret;

    if (meth == NULL)
        meth = NCONF_default();
    
    ret = meth->create(meth);
    if (ret == NULL) {
        CONFerr(0, ERR_R_MALLOC_FAILURE);
        return NULL;
    }
    /* 这个流程中是NULL,不需要分析 */
    ret->libctx = libctx;
    
    return ret;

}

我们先看NCONF_default

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* 标记上这些方法,相关定义后续会给出,且将会使用 */
static CONF_METHOD default_method = {
    "OpenSSL default",
    def_create,
    def_init_default,
    def_destroy,
    def_destroy_data,
    def_load_bio,
    def_dump,
    def_is_number,
    def_to_int,
    def_load
};

CONF_METHOD *NCONF_default(void)
{
    return &default_method;
}

第一个在default_method被使用的方法就是def_create, 很明显是去申请一块CONF结构体内存,之后调用def_init_default去初始化结构体 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static CONF *def_create(CONF_METHOD *meth)
{
    CONF *ret;

    ret = OPENSSL_malloc(sizeof(*ret));
    if (ret != NULL)
        /* 这里调用`def_init_default` */
        if (meth->init(ret) == 0) {
            OPENSSL_free(ret);
            ret = NULL;
        }
    return ret;

}

static int def_init_default(CONF *conf)
{
    if (conf == NULL)
        return 0;

    memset(conf, 0, sizeof(*conf));
    /* 将新申请的CONF结构体的method字段设置为默认method */
    conf->meth = &default_method;
    /* meth_data的设置,这个是.conf文件字符解析时候使用的,我们这里不讲 */
    conf->meth_data = (void *)CONF_type_default;

    return 1;
}
NCONF_load

初始化好CONF结构体,确定好对应配置文件名,开始对配置文件进行解析,NCONF_load (OpenSSL连配置文件格式都自己定义自己解析,硬核硬核)将调用到默认方法之 def_load

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int NCONF_load(CONF *conf, const char *file, long *eline)
{
    if (conf == NULL) {
        CONFerr(CONF_F_NCONF_LOAD, CONF_R_NO_CONF);
        return 0;
    }

    return conf->meth->load(conf, file, eline);

}

static int def_load(CONF *conf, const char *name, long *line)
{
    int ret;
    BIO *in = NULL;
    
    /* 这里通过BIO读入文件(Binary IO, openssl自己定义的io,简单理解就是一块内存Orz) */
#ifdef OPENSSL_SYS_VMS
    in = BIO_new_file(name, "r");
#else
    in = BIO_new_file(name, "rb");
#endif
    ...
    
    /* 正式解析,按段解析;
     * 这里不分析咋解析的,很复杂很长,甚至能处理一些环境变量$(xxx)... 服
     * 最后结果都存在哈希表data中
     */
    ret = def_load_bio(conf, in, line);
    BIO_free(in);

    return ret;
}
CONF_modules_load

核心过程,从CONF去加载第一部分提到的'engines'这个module:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
int CONF_modules_load(const CONF *cnf, const char *appname,
                      unsigned long flags)
{
    STACK_OF(CONF_VALUE) *values;
    CONF_VALUE *vl;
    char *vsection = NULL;

    int ret, i;
    
    if (!cnf)
        return 1;
    
    /* 先获取到对应的section名,这里就是"openssl_conf" */
    if (appname)
        vsection = NCONF_get_string(cnf, NULL, appname);
    
    if (!appname || (!vsection && (flags & CONF_MFLAGS_DEFAULT_SECTION)))
        vsection = NCONF_get_string(cnf, NULL, "openssl_conf");
    
    if (!vsection) {
        ERR_clear_error();
        return 1;
    }
    
    OSSL_TRACE1(CONF, "Configuration in section %s\n", vsection);
    /* 
     * 找到第一个段 openssl_conf
     * [openssl_def]
     * engines = engine_section
     */
    values = NCONF_get_section(cnf, vsection);
    
    if (!values)
        return 0;
    
    for (i = 0; i < sk_CONF_VALUE_num(values); i++) {
        vl = sk_CONF_VALUE_value(values, i);
        /* 遍历所有的value,这里只有一个 'engines' */
        ret = module_run(cnf, vl->name, vl->value, flags);
        OSSL_TRACE3(CONF, "Running module %s (%s) returned %d\n",
                    vl->name, vl->value, ret);
        if (ret <= 0)
            if (!(flags & CONF_MFLAGS_IGNORE_ERRORS))
                return ret;
    }
    
    return 1;

}

static int module_run(const CONF *cnf, const char *name, const char *value,
                      unsigned long flags)
{
    CONF_MODULE *md;
    int ret;

    if (!RUN_ONCE(&load_builtin_modules, do_load_builtin_modules))
        return -1;

    /* 这里会在supported_modules这个栈上找到'engines'这个CONF_MODULE,开始魔幻表演 */
    md = module_find(name);
    
    ...
    /* init这个module,这里将去调用到'dynamic'这个engine,下面将分析 */
    ret = module_init(md, name, value, cnf);
    ...
    return ret;
}

/* initialize a module */
/* 此处将申请上一个所谓的initialized module,
 * 之后调用'engines'的init函数
 * 若成功,将'engines' push进的全局变量栈 initialized_modules */
static int module_init(CONF_MODULE *pmod, const char *name, const char *value,
                       const CONF *cnf)
{
    int ret = 1;
    int init_called = 0;
    CONF_IMODULE *imod = NULL;

    /* Otherwise add initialized module to list */
    imod = OPENSSL_malloc(sizeof(*imod));
    if (imod == NULL)
        goto err;

    imod->pmod = pmod;
    imod->name = OPENSSL_strdup(name); /* 即'engines' */
    imod->value = OPENSSL_strdup(value);
    imod->usr_data = NULL;

    if (!imod->name || !imod->value)
        goto memerr;

    /* Try to initialize module */
    if (pmod->init) {
        /* 调用engines的init,即第一部分提到的int_engine_module_init函数 */
        ret = pmod->init(imod, cnf);
        init_called = 1;
        /* Error occurred, exit */
        if (ret <= 0)
            goto err;
    }

    if (initialized_modules == NULL) {
        initialized_modules = sk_CONF_IMODULE_new_null();
        if (!initialized_modules) {
            CONFerr(CONF_F_MODULE_INIT, ERR_R_MALLOC_FAILURE);
            goto err;
        }
    }
    
    /* 将'engines' push进的全局变量栈 initialized_modules */
    if (!sk_CONF_IMODULE_push(initialized_modules, imod)) {
        CONFerr(CONF_F_MODULE_INIT, ERR_R_MALLOC_FAILURE);
        goto err;
    }

    pmod->links++;

    return ret;

 err:
    ...

}

CONF的第一部分处理完毕,开始查看如何继续解析这个配置

int_engine_module_init

这部分开始取engines这个section下的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int int_engine_module_init(CONF_IMODULE *md, const CONF *cnf)
{
    STACK_OF(CONF_VALUE) *elist;
    CONF_VALUE *cval;
    int i;
    OSSL_TRACE2(CONF, "Called engine module: name %s, value %s\n",
                CONF_imodule_get_name(md), CONF_imodule_get_value(md));
    /* Value is a section containing ENGINEs to configure */
    elist = NCONF_get_section(cnf, CONF_imodule_get_value(md));
    
    /* 
     * 获取engine_section下的列表,这里就一个section叫做engine_x_section 
     *  [engine_section]
     *  engine_x = engine_x_section
     */
    if (!elist) {
        ENGINEerr(ENGINE_F_INT_ENGINE_MODULE_INIT,
                  ENGINE_R_ENGINES_SECTION_ERROR);
        return 0;
    }
    
    for (i = 0; i < sk_CONF_VALUE_num(elist); i++) {
        cval = sk_CONF_VALUE_value(elist, i);
        /* 
         * name: engine_x, value: engine_x_section 
         * 准备开始加载了
         */
        if (!int_engine_configure(cval->name, cval->value, cnf))
            return 0;
    }
    
    return 1;

}

int_engine_configure 是加载engine的主要流程,我们按顺序来一步一步分析内部的循环

int_engine_configure

  1. 首先加载上value的section:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int int_engine_configure(const char *name, const char *value, const CONF *cnf)
{
    int i;
    int ret = 0;
    long do_init = -1;
    STACK_OF(CONF_VALUE) *ecmds;
    CONF_VALUE *ecmd = NULL;
    const char *ctrlname, *ctrlvalue;
    ENGINE *e = NULL;
    int soft = 0;

    name = skip_dot(name);
    OSSL_TRACE1(CONF, "Configuring engine %s\n", name);
    /* Value is a section containing ENGINE commands */
    /* 在conf的哈希表中找 叫做engine_x_section的section */
    ecmds = NCONF_get_section(cnf, value);
    
    /* 
     * 此时ecmds是一个栈,按顺序有以下CONF_VALUE (共有section = "engine_x_section")
     * {.name = "engine_id", .value = "engineX"}
     * {.name = "dynamic_path", .value = "${ENV::PWD}/build/engine_ex.so"(这里已经通配符解析      *  了)}
     * {.name = "default_algorithms", .value = "ALL"}
     * {.name = "init", .value = "1"}
     */
    if (!ecmds) {
        ENGINEerr(ENGINE_F_INT_ENGINE_CONFIGURE,
                  ENGINE_R_ENGINE_SECTION_ERROR);
        return 0;
    }
    ...
}
  1. 按照顺序解析:

    第一个是engine_id:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    static int int_engine_configure(const char *name, const char *value, const CONF *cnf)
    {
     ...
        /* 开始对ecmds中栈上的CONF_VALUE遍历,这部分代码都在这个for循环中 */
        for (i = 0; i < sk_CONF_VALUE_num(ecmds); i++) {
            ecmd = sk_CONF_VALUE_value(ecmds, i);
            /* 解析出ctrlname和ctrlvalue,对应结构体中.name和.value, 下同 */
            ctrlname = skip_dot(ecmd->name);
            ctrlvalue = ecmd->value;
            OSSL_TRACE2(CONF, "ENGINE: doing ctrl(%s,%s)\n",
                        ctrlname, ctrlvalue);
    
            /* First handle some special pseudo ctrls */
    
            /* Override engine name to use */
            if (strcmp(ctrlname, "engine_id") == 0)
                /* 把name制成conf文件中engine_id */
                name = ctrlvalue;
         ...
          }
          ...
    }
    

    第二个是dynamic_path, 这个定义最关键,找到这个name,开始按照指定路径加载动态库engine:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    
    for(...) {
    ...
        else if (strcmp(ctrlname, "dynamic_path") == 0) {
                 /* 
                  * 看到这里是不是豁然开朗,首先找到第二部分初始化的叫做dynamic的engine 
                  * 但这个地方有个值得注意的点,底下分析ENGINE_by_id
                  */
                    e = ENGINE_by_id("dynamic");
                 /* 拿到'dynamic'这个ENGINE结构体后,进行三步操作,完成了engineX这个so的加载 */
                 /* 之后我们将单独把ENGINE_ctrl_cmd_string拿出来分析,观察它是如何去加载的*/
                    if (!e)
                        goto err;
                    if (!ENGINE_ctrl_cmd_string(e, "SO_PATH", ctrlvalue, 0))
                        goto err;
                    if (!ENGINE_ctrl_cmd_string(e, "LIST_ADD", "2", 0))
                        goto err;
                    if (!ENGINE_ctrl_cmd_string(e, "LOAD", NULL, 0))
                        goto err;
            ...
     }
     /* 
      * 完成这三步操作后,'dynamic'副本这个engine已经被重写成了 'engineX'!
      * 同时这个engineX也加入了engines的队列中。
     */
    
     ENGINE *ENGINE_by_id(const char *id)
        {
         /* 入参检查和环境初始化检查 omit */ 
            ...
            /* 加锁后开始遍历链表,匹配id = "dynamic" */
            CRYPTO_THREAD_write_lock(global_engine_lock);
            iterator = engine_list_head;
    
            while (iterator && (strcmp(id, iterator->id) != 0))
                iterator = iterator->next;
            if (iterator != NULL) {
                /*
                 * We need to return a structural reference. If this is an ENGINE
                 * type that returns copies, make a duplicate - otherwise increment
                 * the existing ENGINE's reference count.
                 */
    
                /* 匹配成功后的小操作:看ENGINE_load_dynamic源码可以看到 dynamic->flag 被设置成了                 ENGINE_FLAGS_BY_ID_COPY */
                if (iterator->flags & ENGINE_FLAGS_BY_ID_COPY) {
                    ENGINE *cp = ENGINE_new();
                    if (cp == NULL)
                        iterator = NULL;
                    else {
                        /* 此处很重要! */
                        /* 此处取出的dynamic,不是直接取出链表中的engine节点,而是复制了一个节点 */
                        engine_cpy(cp, iterator);
                        iterator = cp;
                    }
                } else {
                    iterator->struct_ref++;
                    engine_ref_debug(iterator, 0, 1);
                }
           }
            CRYPTO_THREAD_unlock(global_engine_lock);
         if (iterator != NULL)
                /* 作为取出返回值,得到了一个dynamic的副本 */
             return iterator; 
     }
    

注意,此时e这个局部变量已经是一个id'engineX'的ENGINE结构体了,也就是完成了动态加载的engine!

第三步是default_algorithms:

1
2
3
4
5
for (...) {
    else if (strcmp(ctrlname, "default_algorithms") == 0) {
                if (!ENGINE_set_default_string(e, ctrlvalue))
    ...
}

第四步,完成Init:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
for (...) {
 if (strcmp(ctrlname, "init") == 0) {
            if (!NCONF_get_number_e(cnf, value, "init", &do_init))
                goto err;
            if (do_init == 1) {
                /* 
                 * 此处为1,完成engine init, 
                 * 具体代码就是调用ENGINE_init去执行e->init, 增加引用数之类的,我们这里其实是空的 
                 * 之后去把这个engine同时加入initialized_engines这个全局变量栈中。代码不看了
                 */
                if (!int_engine_init(e))
                    goto err;
    ...
}
 

就此CONF_modules_load全部运行完成,engineX加载完毕。后续只需要像main函数中的使用ENGINE_by_id("engineX");就可以取得这个engine了。圆满。

但是 bind_engine 在哪调用的呢,还是没看到,那必然是在ENGINE_ctrl_cmd_string流程中。所以下面重点讲讲这个函数。

ENGINE_ctrl_cmd_string

从cmd_name去获取cmd_num

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
int ENGINE_ctrl_cmd_string(ENGINE *e, const char *cmd_name, const char *arg,
                           int cmd_optional)
{
    int num, flags;
    long l;
    char *ptr;

 ...
     /* 宏的命名已经暴露了一切,通过cmd_name得到cmd_num */
    if (e->ctrl == NULL
        || (num = ENGINE_ctrl(e, ENGINE_CTRL_GET_CMD_FROM_NAME,
                              0, (void *)cmd_name, NULL)) <= 0) {
         ...
    }
    ...
}

int ENGINE_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f) (void))
{
 ...
    /*
     * Intercept any "root-level" commands before trying to hand them on to
     * ctrl() handlers.
     */
    switch (cmd) {
    /* 这部分是通用的ctrl,范围为10 ~ 18, 全部进入int_ctrl_helper */
    case ENGINE_CTRL_HAS_CTRL_FUNCTION:
        return ctrl_exists;
    case ENGINE_CTRL_GET_FIRST_CMD_TYPE:
    case ENGINE_CTRL_GET_NEXT_CMD_TYPE:
    case ENGINE_CTRL_GET_CMD_FROM_NAME:
    case ENGINE_CTRL_GET_NAME_LEN_FROM_CMD:
    case ENGINE_CTRL_GET_NAME_FROM_CMD:
    case ENGINE_CTRL_GET_DESC_LEN_FROM_CMD:
    case ENGINE_CTRL_GET_DESC_FROM_CMD:
    case ENGINE_CTRL_GET_CMD_FLAGS:
        /* 
         * 这里dynamic的flag为ENGINE_FLAGS_BY_ID_COPY,0x0004 
         * ENGINE_FLAGS_MANUAL_CMD_CTRL = 0x0002,与的结果为0
         */
        if (ctrl_exists && !(e->flags & ENGINE_FLAGS_MANUAL_CMD_CTRL))
            return int_ctrl_helper(e, cmd, i, p, f);
        if (!ctrl_exists) {
            ENGINEerr(ENGINE_F_ENGINE_CTRL, ENGINE_R_NO_CONTROL_FUNCTION);
            /*
             * For these cmd-related functions, failure is indicated by a -1
             * return value (because 0 is used as a valid return in some
             * places).
             */
            return -1;
        }
    default:
        break;
    }
    /* Anything else requires a ctrl() handler to exist. */
    /* 这里是确定当前engine->ctrl != NULL */
    if (!ctrl_exists) {
        ENGINEerr(ENGINE_F_ENGINE_CTRL, ENGINE_R_NO_CONTROL_FUNCTION);
        return 0;
    }
    
    /* 调用上面看到的 dynamic->ctrl = dynamic_ctrl, 后面会调用到这来 */
    return e->ctrl(e, cmd, i, p, f);
}

/* 这个函数也将反复调用(吐槽下openssl这鬼之设计),我们这里先看当前的cmd */
static int int_ctrl_helper(ENGINE *e, int cmd, long i, void *p,
                           void (*f) (void))
{
    int idx;
    char *s = (char *)p;
    const ENGINE_CMD_DEFN *cdp;
 ...
        
    /* Now handle cmd_name -> cmd_num conversion */
    if (cmd == ENGINE_CTRL_GET_CMD_FROM_NAME) {
        /* 从dynamic的cmd_defns中去匹配cmd_name,假设是"SO_PATH", 
           直接去查第二部分的dynamic_cmd_defns,刚好匹配上idx = 0 */
        if ((e->cmd_defns == NULL)
            || ((idx = int_ctrl_cmd_by_name(e->cmd_defns, s)) < 0)) {
            ENGINEerr(ENGINE_F_INT_CTRL_HELPER, ENGINE_R_INVALID_CMD_NAME);
            return -1;
        }
        /* 查idx = 0时的 cmd_num = 200 = DYNAMIC_CMD_SO_PATH */
        return e->cmd_defns[idx].cmd_num;
    }
 ...
}

可以看到这里的num返回回来的DYNAMIC_CMD_SO_PATH,是靠dynamic.cmd_defns中的ENGINE_CMD_DEFN数组表查询得到的。往下接着看ENGINE_ctrl_cmd_string

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int ENGINE_ctrl_cmd_string(ENGINE *e, const char *cmd_name, const char *arg,
                           int cmd_optional)

{
    /* 继续调用公用ctrl,进入到int_ctrl_helper
       (看底下开源的注释,两个函数做的ctrl操作一样的,为啥这么搞也许就是未解之谜吧) */
    ...
    if (!ENGINE_cmd_is_executable(e, num)) {
        ENGINEerr(ENGINE_F_ENGINE_CTRL_CMD_STRING,
                  ENGINE_R_CMD_NOT_EXECUTABLE);
        return 0;
    }
 
    /* 顾名思义,拿到dynamic的flag,这里将得到idx = 0时,cmd_defns表中0处的第四个元素 */
    flags = ENGINE_ctrl(e, ENGINE_CTRL_GET_CMD_FLAGS, num, NULL, NULL);
    if (flags < 0) {
        /*
         * Shouldn't happen, given that ENGINE_cmd_is_executable() returned
         * success.
         */
        ENGINEerr(ENGINE_F_ENGINE_CTRL_CMD_STRING,
                  ENGINE_R_INTERNAL_LIST_ERROR);
        return 0;
    }
}

static int int_ctrl_helper(ENGINE *e, int cmd, long i, void *p,
                           void (*f) (void))
{
 ...
    if ((e->cmd_defns == NULL)
        || ((idx = int_ctrl_cmd_by_num(e->cmd_defns, (unsigned int)i)) < 0)) {
        ENGINEerr(ENGINE_F_INT_CTRL_HELPER, ENGINE_R_INVALID_CMD_NUMBER);
        return -1;
    }
    /* Now the logic splits depending on command type */
    cdp = &e->cmd_defns[idx];
    switch (cmd) {
 ...
    case ENGINE_CTRL_GET_CMD_FLAGS:
        /* 可以查出来上面的是 ENGINE_CMD_FLAG_STRING = 0x0002 */
        return cdp->cmd_flags;
    }
 ...
}

别问为啥不一次查出来,要多次遍历,问就是架构。继续看ENGINE_ctrl_cmd_string,终于要做真正的操作了, 可以看到,最后进入了dynamic_ctrl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int ENGINE_ctrl_cmd_string(ENGINE *e, const char *cmd_name, const char *arg,
                           int cmd_optional)

{
    ... 
    /* ENGINE_CMD_FLAG_NO_INPUT = 0x0004 */
  if (flags & ENGINE_CMD_FLAG_NO_INPUT) {
        /* 如果命令查出来的flag应该没有arg_input, 但arg非空,直接退出???? */
        if (arg != NULL) {
            ENGINEerr(ENGINE_F_ENGINE_CTRL_CMD_STRING,
                      ENGINE_R_COMMAND_TAKES_NO_INPUT);
            return 0;
        }
        /*
         * We deliberately force the result of ENGINE_ctrl() to 0 or 1 rather
         * than returning it as "return data". This is to ensure usage of
         * these commands is consistent across applications and that certain
         * applications don't understand it one way, and others another.
         */
        /* 最后"LOAD"命令走的这 */
        if (ENGINE_ctrl(e, num, 0, (void *)arg, NULL) > 0)
            return 1;
        return 0;
    }
    /* So, we require input */
    if (arg == NULL) {
        ENGINEerr(ENGINE_F_ENGINE_CTRL_CMD_STRING,
                  ENGINE_R_COMMAND_TAKES_INPUT);
        return 0;
    }
    /* 一定有更好的写法吧,这种判断也太迷惑了。。 */
    /* If it takes string input, that's easy */
    if (flags & ENGINE_CMD_FLAG_STRING) {
        /* Same explanation as above */
        /* 所以应该调用到这,注意此时num 将大于200, 肯定不是默认的流程,
           这就走到了return e->ctrl(e, cmd, i, p, f); 即 dynamic_ctrl */
        if (ENGINE_ctrl(e, num, 0, (void *)arg, NULL) > 0)
            return 1;
        return 0;
    }
    
    /* 此时arg是数字,需要从str转int,LIST_ADD走这 */
     if (!(flags & ENGINE_CMD_FLAG_NUMERIC)) {
         ENGINEerr(ENGINE_F_ENGINE_CTRL_CMD_STRING,
                   ENGINE_R_INTERNAL_LIST_ERROR);
         return 0;
     }

     l = strtol(arg, &ptr, 10);
     if ((arg == ptr) || (*ptr != '\0')) {
         ENGINEerr(ENGINE_F_ENGINE_CTRL_CMD_STRING,
                   ENGINE_R_ARGUMENT_IS_NOT_A_NUMBER);
         return 0;
     }
     /*
      * Force the result of the control command to 0 or 1, for the reasons
      * mentioned before.
      */
     if (ENGINE_ctrl(e, num, l, NULL, NULL) > 0)
         return 1;
 ...
}

所以这个函数的主要步骤就是根据输入的cmd_namedynamic中挂载的cmd_defns取出对应的cmd_numflag,之后用cmd_num调用到dynamic挂载的ctrl字段函数去做真正的操作。我们用一张表统计下三次取到的结果:

cmd_name cmd_num flag
“SO_PATH” DYNAMIC_CMD_SO_PATH = 200 ENGINE_CMD_FLAG_STRING 0x0002
“LIST_ADD” DYNAMIC_CMD_LIST_ADD = 203 ENGINE_CMD_FLAG_NUMERIC 0x0001
“LOAD” DYNAMIC_CMD_LOAD = 206 ENGINE_CMD_FLAG_NO_INPUT 0x0004

根据这个表,我们去看对于dynamic->ctrldynamic_ctrl函数对这几个cmd的操作

dynamic_ctrl

先看这个函数的公共部分,对相同的engine会初始化上一个ctx上下文:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/* 动态库加载的上下文 */
struct st_dynamic_data_ctx {
    /* The DSO object we load that supplies the ENGINE code */
    DSO *dynamic_dso;
    /*
     * The function pointer to the version checking shared library function
     */
    dynamic_v_check_fn v_check;
    /*
     * The function pointer to the engine-binding shared library function
     */
    dynamic_bind_engine bind_engine;
    /* The default name/path for loading the shared library */
    char *DYNAMIC_LIBNAME;
    /* Whether to continue loading on a version check failure */
    int no_vcheck;
    /* If non-NULL, stipulates the 'id' of the ENGINE to be loaded */
    char *engine_id;
    /*
     * If non-zero, a successfully loaded ENGINE should be added to the
     * internal ENGINE list. If 2, the add must succeed or the entire load
     * should fail.
     */
    int list_add_value;
    /* The symbol name for the version checking function */
    const char *DYNAMIC_F1;
    /* The symbol name for the "initialise ENGINE structure" function */
    const char *DYNAMIC_F2;
    /*
     * Whether to never use 'dirs', use 'dirs' as a fallback, or only use
     * 'dirs' for loading. Default is to use 'dirs' as a fallback.
     */
    int dir_load;
    /* A stack of directories from which ENGINEs could be loaded */
    STACK_OF(OPENSSL_STRING) *dirs;
};

static int dynamic_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f) (void))
{
    /* 这个函数将会初始化并保存动态库数据的ctx,这也是为什么可以反复调用这个接口的原因 */
    dynamic_data_ctx *ctx = dynamic_get_data_ctx(e);
    int initialised;

    if (!ctx) {
        ENGINEerr(ENGINE_F_DYNAMIC_CTRL, ENGINE_R_NOT_LOADED);
        return 0;
    }
    
    /* 可以看到,加载完成的标志是dynamic_dso钩子已经挂上了 */
    initialised = ((ctx->dynamic_dso == NULL) ? 0 : 1);
    /* All our control commands require the ENGINE to be uninitialised */
    if (initialised) {
        ENGINEerr(ENGINE_F_DYNAMIC_CTRL, ENGINE_R_ALREADY_LOADED);
        return 0;
    }
   
    /* cmd解析,底下逐个分析 */
    ...
}

/*
 * This function retrieves the context structure from an ENGINE's "ex_data",
 * or if it doesn't exist yet, sets it up.
 */
static dynamic_data_ctx *dynamic_get_data_ctx(ENGINE *e)
{
    dynamic_data_ctx *ctx;
    if (dynamic_ex_data_idx < 0) {
        /*
         * Create and register the ENGINE ex_data, and associate our "free"
         * function with it to ensure any allocated contexts get freed when
         * an ENGINE goes underground.
         */
        int new_idx = ENGINE_get_ex_new_index(0, NULL, NULL, NULL,
                                              dynamic_data_ctx_free_func);
        if (new_idx == -1) {
            ENGINEerr(ENGINE_F_DYNAMIC_GET_DATA_CTX, ENGINE_R_NO_INDEX);
            return NULL;
        }
        CRYPTO_THREAD_write_lock(global_engine_lock);
        /* Avoid a race by checking again inside this lock */
        if (dynamic_ex_data_idx < 0) {
            /* Good, someone didn't beat us to it */
            dynamic_ex_data_idx = new_idx;
            new_idx = -1;
        }
        CRYPTO_THREAD_unlock(global_engine_lock);
        /*
         * In theory we could "give back" the index here if (new_idx>-1), but
         * it's not possible and wouldn't gain us much if it were.
         */
    }
    ctx = (dynamic_data_ctx *)ENGINE_get_ex_data(e, dynamic_ex_data_idx);
    /* Check if the context needs to be created */
    if ((ctx == NULL) && !dynamic_set_data_ctx(e, &ctx))
        /* "set_data" will set errors if necessary */
        return NULL;
    return ctx;
}

/* 
 * 简单的说就是去查挂在engine->ex_data,
 * 这个就是动态库加载的上下文,ex_data是个栈可能有多个上下文,
 * 根据一个全局变量dynamic_ex_data_idx确定当前使用上下文
 * 当然第一次调用ctx是空的,所以需要调用一下dynamic_set_data_ctx初始化
 */
static int dynamic_set_data_ctx(ENGINE *e, dynamic_data_ctx **ctx)
{
    /* 申请ctx的mem */
    dynamic_data_ctx *c = OPENSSL_zalloc(sizeof(*c));
    int ret = 1;

    if (c == NULL) {
        ENGINEerr(ENGINE_F_DYNAMIC_SET_DATA_CTX, ERR_R_MALLOC_FAILURE);
        return 0;
    }
    c->dirs = sk_OPENSSL_STRING_new_null();
    if (c->dirs == NULL) {
        ENGINEerr(ENGINE_F_DYNAMIC_SET_DATA_CTX, ERR_R_MALLOC_FAILURE);
        OPENSSL_free(c);
        return 0;
    }
    /* 初始化一些字段,下面总结 */ 
    c->DYNAMIC_F1 = "v_check", ;
    c->DYNAMIC_F2 = "bind_engine";
    c->dir_load = 1;
    CRYPTO_THREAD_write_lock(global_engine_lock);
    /* 第一次进来为NULL(然而正常是为ctx = NULL才会调用这个函数,可能是冗余校验)*/
    if ((*ctx = (dynamic_data_ctx *)ENGINE_get_ex_data(e,
                                                       dynamic_ex_data_idx))
        == NULL) {
        /* Good, we're the first */
        /* 把ctx挂在engine->ex_data上 */
        ret = ENGINE_set_ex_data(e, dynamic_ex_data_idx, c);
        if (ret) {
            *ctx = c;
            c = NULL;
        }
    }
    CRYPTO_THREAD_unlock(global_engine_lock);
    /*
     * If we lost the race to set the context, c is non-NULL and *ctx is the
     * context of the thread that won.
     */
    if (c)
        sk_OPENSSL_STRING_free(c->dirs);
    OPENSSL_free(c);
    return ret;
}

/* 
 * 得到最后的结果 dynamic->ex_data = ctx;
 * ctx = {.DYNAMIC_F1 = "v_check", .DYNAMIC_F2 = "bind_engine", c->dir_load = 1}
 * 惊奇的发现了 bind_engine 虽然他只是个字符串,但是我相信你已经知道原因了
 * 他需要在动态库中去寻找这个符号
 */

之后我们逐一分析这三个cmd

DYNAMIC_CMD_SO_PATH和DYNAMIC_CMD_LIST_ADD

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int dynamic_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f) (void))
{
    ...
    switch (cmd) {
    /* 注意, p就是ctrlvalue,即从conf中取下来的值 */
    case DYNAMIC_CMD_SO_PATH:
        /* a NULL 'p' or a string of zero-length is the same thing */
        if (p && (strlen((const char *)p) < 1))
            p = NULL;
        OPENSSL_free(ctx->DYNAMIC_LIBNAME);
        if (p)
            /* 很明显只是做了个简单的复制,此时路径已经赋值上了 */
            ctx->DYNAMIC_LIBNAME = OPENSSL_strdup(p);
        else
            ctx->DYNAMIC_LIBNAME = NULL;
        return (ctx->DYNAMIC_LIBNAME ? 1 : 0);
    case DYNAMIC_CMD_LIST_ADD:
        if ((i < 0) || (i > 2)) {
           ENGINEerr(ENGINE_F_DYNAMIC_CTRL, ENGINE_R_INVALID_ARGUMENT);
           return 0;
        }
        /* 很简单,赋值而已 */
        ctx->list_add_value = (int)i;
        return 1;
    ... 
    }
}

这两个都很简单,最后难点都给了LOAD

DYNAMIC_CMD_LOAD

最关键的函数,完成了全部的加载,解释都在注释里:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
static int dynamic_ctrl(ENGINE *e, int cmd, long i, void *p, void (*f) (void))
{
    ...
    switch (cmd) {
    case DYNAMIC_CMD_LOAD:
        return dynamic_load(e, ctx);    
    ... 
    }
}

static int dynamic_load(ENGINE *e, dynamic_data_ctx *ctx)
{
    ENGINE cpy;
    dynamic_fns fns;
    
    /* 
     * 先new一个DSO结构体,DSO这一套函数怎么玩的这里先不讲了,
     * 可以理解为内部也有一个加载钩子,有4个挂载点,估计再展开讲读者疯了
     */
    if (ctx->dynamic_dso == NULL)
        ctx->dynamic_dso = DSO_new();
    if (ctx->dynamic_dso == NULL)
        return 0;
    /* 此处检查DYNAMIC_LIBNAME不能为空,这个就是dso的加载地址 */
    if (!ctx->DYNAMIC_LIBNAME) {
        if (!ctx->engine_id)
            return 0;
        DSO_ctrl(ctx->dynamic_dso, DSO_CTRL_SET_FLAGS,
                 DSO_FLAG_NAME_TRANSLATION_EXT_ONLY, NULL);
        ctx->DYNAMIC_LIBNAME =
            DSO_convert_filename(ctx->dynamic_dso, ctx->engine_id);
    }
    
    /* 核心加载函数int_load,看下面分析 */
    if (!int_load(ctx)) {
        ENGINEerr(ENGINE_F_DYNAMIC_LOAD, ENGINE_R_DSO_NOT_FOUND);
        DSO_free(ctx->dynamic_dso);
        ctx->dynamic_dso = NULL;
        return 0;
    }
    
    /* We have to find a bind function otherwise it'll always end badly */
    /* 
     * 此时engine动态库已经加载如内存,符号表与对应地址也准备完成 
     * 所以肯定是需要去寻找这个绑定engine完成加载的函数了,胜利的曙光
     * DSO_bind_func会在符号表中去匹配第二个参数字符串,这里就是我们要的"bind_engine"
     * 并返回上它的函数地址,挂载在ctx->bind_engine上
     */
    if (!
        (ctx->bind_engine =
         (dynamic_bind_engine) DSO_bind_func(ctx->dynamic_dso,
                                             ctx->DYNAMIC_F2))) {
        ctx->bind_engine = NULL;
        DSO_free(ctx->dynamic_dso);
        ctx->dynamic_dso = NULL;
        ENGINEerr(ENGINE_F_DYNAMIC_LOAD, ENGINE_R_DSO_FAILURE);
        return 0;
    }
    /* Do we perform version checking? */
    if (!ctx->no_vcheck) {
        unsigned long vcheck_res = 0;
        /*
         * Now we try to find a version checking function and decide how to
         * cope with failure if/when it fails.
         */
        ctx->v_check =
            (dynamic_v_check_fn) DSO_bind_func(ctx->dynamic_dso,
                                               ctx->DYNAMIC_F1);
        if (ctx->v_check)
            vcheck_res = ctx->v_check(OSSL_DYNAMIC_VERSION);
        /*
         * We fail if the version checker veto'd the load *or* if it is
         * deferring to us (by returning its version) and we think it is too
         * old.
         */
        if (vcheck_res < OSSL_DYNAMIC_OLDEST) {
            /* Fail */
            ctx->bind_engine = NULL;
            ctx->v_check = NULL;
            DSO_free(ctx->dynamic_dso);
            ctx->dynamic_dso = NULL;
            ENGINEerr(ENGINE_F_DYNAMIC_LOAD,
                      ENGINE_R_VERSION_INCOMPATIBILITY);
            return 0;
        }
    }
    /*
     * First binary copy the ENGINE structure so that we can roll back if the
     * hand-over fails
     */
    memcpy(&cpy, e, sizeof(ENGINE));
    /*
     * Provide the ERR, "ex_data", memory, and locking callbacks so the
     * loaded library uses our state rather than its own. FIXME: As noted in
     * engine.h, much of this would be simplified if each area of code
     * provided its own "summary" structure of all related callbacks. It
     * would also increase opaqueness.
     */
    fns.static_state = ENGINE_get_static_state();
    CRYPTO_get_mem_functions(&fns.mem_fns.malloc_fn, &fns.mem_fns.realloc_fn,
                             &fns.mem_fns.free_fn);
    /*
     * Now that we've loaded the dynamic engine, make sure no "dynamic"
     * ENGINE elements will show through.
     */
    engine_set_all_null(e);

    /* Try to bind the ENGINE onto our own ENGINE structure */
    /* !!!!Attension, 终于调用成功了,我们的engineX终于被设置好了! */
    if (!ctx->bind_engine(e, ctx->engine_id, &fns)) {
        ctx->bind_engine = NULL;
        ctx->v_check = NULL;
        DSO_free(ctx->dynamic_dso);
        ctx->dynamic_dso = NULL;
        ENGINEerr(ENGINE_F_DYNAMIC_LOAD, ENGINE_R_INIT_FAILED);
        /* Copy the original ENGINE structure back */
        memcpy(e, &cpy, sizeof(ENGINE));
        return 0;
    }
    /* Do we try to add this ENGINE to the internal list too? */
    /* 把这个engine的副本add进上面engine全局链表,大功告成!*/
    if (ctx->list_add_value > 0) {
        if (!ENGINE_add(e)) {
            /* Do we tolerate this or fail? */
            if (ctx->list_add_value > 1) {
                /*
                 * Fail - NB: By this time, it's too late to rollback, and
                 * trying to do so allows the bind_engine() code to have
                 * created leaks. We just have to fail where we are, after
                 * the ENGINE has changed.
                 */
                ENGINEerr(ENGINE_F_DYNAMIC_LOAD,
                          ENGINE_R_CONFLICTING_ENGINE_ID);
                return 0;
            }
            /* Tolerate */
            ERR_clear_error();
        }
    }
    return 1;
}

static int int_load(dynamic_data_ctx *ctx)
{
    int num, loop;
    /* Unless told not to, try a direct load */
    /* 
     * DSO_load去打开ctx->DYNAMIC_LIBNAME,把egine对应的lib库加载进内存
     * 解析符号表和对应地址到上面申请好的ctx->dynamic_dso结构体中
     */
    if ((ctx->dir_load != 2) && (DSO_load(ctx->dynamic_dso,
                                          ctx->DYNAMIC_LIBNAME, NULL,
                                          0)) != NULL)
        return 1;
    /* If we're not allowed to use 'dirs' or we have none, fail */
    if (!ctx->dir_load || (num = sk_OPENSSL_STRING_num(ctx->dirs)) < 1)
        return 0;
    for (loop = 0; loop < num; loop++) {
        /* 还有链接的dso这里会处理递归的去加载,对应的需要在ctx->dirs中 */
        const char *s = sk_OPENSSL_STRING_value(ctx->dirs, loop);
        char *merge = DSO_merge(ctx->dynamic_dso, ctx->DYNAMIC_LIBNAME, s);
        if (!merge)
            return 0;
        if (DSO_load(ctx->dynamic_dso, merge, NULL, 0)) {
            /* Found what we're looking for */
            OPENSSL_free(merge);
            return 1;
        }
        OPENSSL_free(merge);
    }
    return 0;
}

终于终于终于,找到目标了,这个叫做'dynamic'的engine副本完成了变成engineX的蜕变。

后续

难怪这么多人喷OpenSSL烂,这复杂的流程,这一个又一个的钩子。不过这一串源码读下来看明白的时候还是有神清气爽的感觉。

有缘后面会分析密码算法具体挂载,如ENGINE_set_digests

我很菜,有错误的地方欢迎指正


Techsum
Techsum
A useless security engineer/cn:帅华