博客 / 詳情

返回

OpenSSL 3.0.0 設計(二)|Core 和 Provider 設計

譯|王祖熙(花名:金九 )

螞蟻集團開發工程師
負責國產化密碼庫 Tongsuo 的開發和維護

專注於密碼學、高性能網絡、網絡安全等領域

本文 6132 字 閲讀 15 分鐘

本文翻譯 OpenSSL 官網文檔:https://www.openssl.org/docs/OpenSSL300Design.html

Tongsuo-8.4.0 是基於 OpenSSL-3.0.3 開發,所以本文對 Tongsuo 開發者同樣適用,內容豐富,值得一讀!

由於文章篇幅較長,今天帶來的是 《Core 和 Provider 設計》 部分內容,上一篇《介紹、術語和架構》可查看已發佈過的內容。後續內容將隨每週推送完整發布,請持續關注銅鎖

Core 和 Provider 設計

下圖顯示了與 Core 和 Provider 設計相關的交互,有四個主要組件:用户應用程序、EVP 組件、Core 和密碼 Provider (可能有多個 Provider,但在此不相關)

圖片

Core 具有以下特點:

  • 實現 Provider 的發現、加載、初始化和卸載功能
  • 支持基於屬性的算法查詢
  • 實現了算法查詢和實現細節的緩存
  • 在庫上下文中運行,其中包含全局屬性、搜索緩存和分派表等數據

Provider 具有以下特點:

  • 提供對特定算法實現的訪問
  • 將算法實現與一組明確定義的屬性相關聯
  • 以一種與具體實現無關的方式支持參數傳遞
  • 可以在任何時間點進行加載
  • 具有眾所周知的模塊入口點

接下來的小節描述了應用程序使用的流程,以加載 Provider、獲取算法實現並使用它為例。此外,本節詳細描述了算法、屬性和參數的命名方式,如何處理算法查詢、註冊和初始化算法,以及如何加載 Provider。

為了使應用程序能夠使用算法,首先必須通過算法查詢來“獲取 (fetch) ”其實現。我們的設計目標是能夠支持顯式 (事先) 獲取算法和在使用時獲取算法的方式。默認情況下,我們希望在使用時進行獲取 (例如使用 EVP_sha256() ,這樣算法通常會在 init 函數期間進行獲取,並綁定到上下文對象 (通常命名為 ctx 。顯式獲取選項將通過新的 API 調用實現 (例如 EVP_MD_fetch()

上述圖示展示了顯式獲取算法的方法。具體步驟如下:

  1. 需要加載每個 Provider,這將隱式發生 (默認 Provider 或通過配置指定) ,也可以由應用程序顯式請求加載。加載過程包括動態共享對象的加載 (根據需要) 和初始化。
  • Core 組件將模塊物理加載到內存中。 (如果默認 Provider 已經在內存中,則無需加載)
  • Core 組件調用 Provider 的入口點,以便 Provider 對自身進行初始化。

    在入口點函數中,Provider 使用從 Core 組件傳入的值初始化一些 Provider 變量。如果初始化成功,Provider 將返回一個用於 Provider 算法實現查詢的回調函數給 Core 組件。

  1. 用户應用程序通過調用獲取例程請求算法。
  • EVP 將全局屬性與調用特定屬性以及算法標識相結合,以找到相應的算法實現,然後創建並返回一個庫句柄 (例如 EVP_MDEVP_CIPHER 給應用程序。

    在內部緩存中進行第一次實現調度表的搜索。

    如果第一次搜索失敗,則通過詢問 Provider 是否具有符合查詢屬性的算法實現來進行第二次搜索,當完成此搜索時,除非 Provider 選擇不進行緩存 (用於第一次搜索 2.1.1) ,否則結果數據將被緩存。例如,PKCS#11 Provider 可能選擇不進行緩存,因為其算法可能隨時間可用和不可用。

  1. 然後,用户應用程序通過 EVP API (例如 EVP_DigestInit()EVP_DigestUpdate()EVP_DigestFinal()等) 使用算法。
  • 函數指針被調用,並最終進入 Provider 的實現,執行請求的密碼算法。

對於現有的 EVP_{algorithm}() 函數 (例如 EVP_sha256() 等) ,大部分情況下保持不變。特別是,當 EVP_{algorithm}() 調用返回時,並不會立即執行獲取算法的操作,而是在將上下文對象 (例如 EVP_MD_CTX 綁定到相應的 EVP 初始化函數內部時隱式地進行。具體來説,步驟 2.1 發生在步驟 3.1 之前,這被稱為 "隱式獲取",隱式獲取總是在默認的庫上下文中進行操作。

方法調度表是一個由<函數 ID,函數指針>對組成的列表,其中函數 ID 是 OpenSSL 公開定義並已知的,同時還包括一組用於標識每個特定實現的屬性。Core 可以根據屬性查詢找到相應的調度表,以供適用的操作使用。這種方法允許 Provider 靈活地傳遞函數引用,以便 OpenSSL 代碼可以動態創建其方法結構。

Provider 可以在任何時間點加載,也可以在任何時間點請求卸載。在卸載 Provider 時,應用程序需要確保該 Provider 當前未被使用或引用,如果嘗試使用不再可用的實現,則會返回錯誤信息。

關於 EVP_{algorithm}() 函數的返回值,目前應用程序可以做出的假設是:

  • 常量指針
  • 不需要由應用程序釋放
  • 可以安全地進行比較,用於檢查算法是否相同 (即特定比較 EVP_CIPHEREVP_MD 等指針)

對於應用程序直接使用顯式獲取 (而不是使用現有的 EVP_{algorithm}() 函數) 的情況,語義將有所不同:

  • 非常量指針
  • 需要由應用程序釋放
  • 指針之間不能安全地進行比較 (後文將詳細説明)

將提供新的 API 來測試可以用於顯式獲取對象和靜態變體對象的相等性,這些 API 將使得可以比較算法標識本身或具體的算法實現。

庫上下文

庫上下文是一個不透明的結構,用於保存庫的“全局”數據,OpenSSL 將提供這樣的結構,僅限於 Core 必須保留的全局數據,未來的擴展可能會包括其他現有的全局數據,應用程序可以創建和銷燬一個或多個庫上下文,所有後續與 Core 的交互都將在其中進行,如果應用程序不創建並提供自己的庫上下文,則將使用內部的默認上下文。

OPENSSL_CTX *OPENSSL_CTX_new();
void OPENSSL_CTX_free(OPENSSL_CTX *ctx);

庫上下文可以傳遞給顯式獲取函數。如果將 NULL 傳遞給它們,將使用內部默認上下文。

可以分配多個庫上下文,這意味着任何 Provider 模塊可能會被初始化多次,這使得應用程序既可以直接鏈接到 libcrypto 並加載所需的 Provider,又可以鏈接到使用其自己 Provider 模塊的其他庫,而二者是相互獨立的。

命名

算法、參數和屬性需要命名,為了確保一致性,並使外部 Provider 實現者能夠以一致的方式定義新名稱,將建立一個推薦或已使用名稱的註冊表。它將與源代碼分開維護。

需要能夠定義名稱的別名,因為在某些情況下,對同一事物存在多個名稱 (例如對於具有通用名稱和 NIST 名稱的橢圓曲線) 的上下文。

算法實現選擇屬性

算法實現 (包括加密和非加密) 將具有一些屬性,用於從可用的實現中選擇一個實現。在 3.0 版本中,定義了兩個屬性:

  • 該實現是否為默認實現?
  • 該實現是否經過 FIPS 驗證?

有效的輸入及其含義如下:

圖片

在所有情況下,屬性名稱將被定義為可打印的 ASCII 字符,並且不區分大小寫,屬性值可以帶引號或不帶引號,不帶引號的值也必須是可打印的 ASCII 字符,並且不區分大小寫,引號中的值僅以原始字節比較的方式進行相等性測試。

Provider 將能夠提供自己的名稱或值,屬性定義和查詢的完整語法見附錄 1-屬性語法。

OpenSSL 保留所有沒有句點的屬性名稱;供應商提供的屬性名稱必須在名稱中包含句點。預期 (但不強制要求) 屬性名稱中第一個句點之前的部分是供應商的名稱或與之相關的內容,以通過命名空間提供一定程度的衝突避免。

在開發此版本的過程中,可能會定義其他屬性,一個可能的候選是 Provider,表示提供實現的 Provider 名稱。另一個可能性是 engine,表示此算法由偽裝為 Provider 的 OpenSSL 1.1.1 動態加載的引擎實現。

將有一個內置的全局屬性查詢字符串,其值為"default"。

屬性選擇算法

算法實現的選擇基於屬性。

Provider 在其提供的算法上設置屬性,應用程序在算法選擇過程中設置要用作篩選條件的屬性查詢。

可以在以下位置指定獲取算法實現所需的屬性:

  • 全局配置文件中的全局設置
  • 基於 API 調用的全局設置
  • 針對特定對象的每個對象的屬性設置。例如 SSL_CTX,SSL

屬性將在算法查找過程中使用 (參數規範的屬性值)

屬性集將以解析為每個指定屬性 (關鍵字) 的屬性的單個值的方式進行評估。關鍵字評估的優先順序如下:

  1. 獲取的每個對象或直接指定的 API 參數
  2. 通過 API 調用設置的全局 (默認) 屬性
  3. 在配置文件中設置的全局 (默認) 屬性

在開發過程中,可能會定義其他屬性設置方法和評估方法。

默認情況下,OpenSSL 3.0 將自動加載配置文件 (其中包含全局屬性和其他設置) ,而無需顯式的應用程序 API 調用,這將在 libcrypto 中發生。請注意,在 OpenSSL 1.1.1 中,配置文件僅在默認 (自動) 初始化 libssl 時自動加載。

參數定義

OpenSSL Core 和 Provider 在保持 OpenSSL 和 Provider 結構不透明的同時需要交換數據,所有複合值將作為項目數組傳遞,使用附錄 2-參數傳遞 (後續將更新) 中定義的公共數據結構,參數將使用它們的名稱 (作為字符串) 進行標識,每個參數包含自己的類型和大小信息。

Core 將定義一個 API,用於將參數值數組或值請求傳遞給 Provider 或特定的算法實現,對於後者,還有由該實現處理的相關對象,對於基本機器類型,可以開發宏來輔助構建和提取值。

操作和操作函數定義

雖然算法和參數名稱基本上由 Provider 控制和分配,但由 libcrypto 調用的操作和相關函數基本上由 Core 控制和分配。

對於僅由 Core 控制的內容,我們將使用宏來命名它們,使用數字作為索引值,分配的索引值是遞增的,即對於任何新的操作或函數,將選擇下一個可用的數字。

算法查詢

每種算法類型 (例如 EVP_MDEVP_CIPHER 等) 都有一個可用的“fetch”函數(例如 EVP_MD_fetch()EVP_CIPHER_fetch() ,算法實現是通過其名稱和屬性來識別的。

如前文 (Core 和 Provider 設計) 中所述,每個 fetch 函數將使用 Core 提供的服務來找到適合的實現,如果找到適當的實現,它將被構造成適當的算法結構 (例如 EVP_MDEVP_CIPHER 並返回給調用應用程序。

如果多個實現與傳遞的名稱和屬性完全匹配,其中之一將在檢索時返回,但具體返回哪個實現是不確定的,此外,並不能保證每次都返回相同的匹配實現。

算法查詢緩存

算法查詢將與其結果一起被緩存。

下列這些算法查詢緩存都可以清除:

  • 返回特定算法實現的所有查詢
  • 來自特定 Provider 的所有算法實現
  • 所有算法實現

多級查詢

為了處理全局屬性和傳遞給特定調用 (例如獲取調用) 的屬性,全局屬性查詢設置將與傳遞的屬性設置合併,除非存在衝突,具體規則如下:

圖片

Provider 模塊加載

Provider 可以是內置的或可動態加載的模塊。

所有算法都是由 Provider 實現的,OpenSSL Core 最初未加載任何 Provider,因此沒有可用的算法,需要查找和加載 Provider,隨後,Core 可以在稍後的時間查詢其中包含的算法實現,這些查詢可能會被緩存。

如果在第一次獲取 (隱式或顯式) 時尚未加載任何 Provider,則會自動加載內置的默認 Provider。

請注意,Provider 可能針對 libcrypto 當前版本之前的舊版本 Core API 進行編寫,例如,用户可以運行與 OpenSSL 主版本不同的 FIPS Provider 模塊版本,這意味着 Core API 必須保持穩定和向後兼容 (就像任何其他公共 API 一樣)

OpenSSL 構建的所有命令行應用程序都將獲得一個 -provider xxx 選項,用於加載 Provider,該選項可以在命令行上多次指定 (可以始終加載多個 Provider) ,並且如果 Provider 在特定操作中未使用 (例如在進行 SHA256 摘要時加載僅提供 AES 的 Provider) ,並不會導致錯誤。

查找和加載動態 Provider 模塊

動態 Provider 模塊在 UNIX 類型操作系統上是 .so 文件,在 Windows 類型操作系統上是 .dll 文件,或者在其他操作系統上對應的文件類型。默認情況下,它們將被安裝在一個眾所周知的目錄中。

Provider 模塊的加載可以通過以下幾種方式進行:

  • 按需加載,應用程序必須明確指定要加載的 Provider 模塊。
  • 通過配置文件加載,加載的 Provider 模塊集合將在配置文件中指定。

其中一些方法可以進行組合使用。

Provider 模塊可以通過完整路徑指定,因此即使它不位於眾所周知的目錄中,也可以加載。

Core 加載 Provider 模塊後,會調用 Provider 模塊的入口點函數。

Provider 模塊入口點

一個 Provider 模塊必須具有以下眾所周知的入口點函數:

int OSSL_provider_init(const OSSL_PROVIDER *provider,
                       const OSSL_DISPATCH *in,
                       const OSSL_DISPATCH **out
                       void **provider_ctx);

如果動態加載的對象中不存在該入口點,則它不是一個有效的模塊,加載會失敗。

in 是核心傳遞給 Provider 的函數數組。

out 是 Provider 傳遞迴 Core 的 Provider 函數數組。

provider_ctx(在本文檔的其他地方可能會縮寫為 provctx 是 Provider 可選創建的對象,用於自身使用 (存儲它需要安全保留的數據) ,這個指針將傳遞迴適當的 Provider 函數。

provider 是指向 Core 所屬 Provider 對象的句柄,它可以作為唯一的 Provider 標識,在某些 API 調用中可能需要,該對象還將填充各種數據,如模塊路徑、Provider 的 NCONF 配置結構 (瞭解如何實現可參見後文 CONF / NCONF 值作為參數的示例) ,Provider 可以使用 Core 提供的參數獲取回調來檢索這些各種值,類型 OSSL_PROVIDER 是不透明的。

OSSL_DISPATCH 是一個開放結構,實現了前文介紹中提到的 <函數 ID,函數指針> 元組。

typedef struct ossl_dispatch_st {
    int function_id;
    void *(*function)();
} OSSL_DISPATCH;

function_id 標識特定的函數,function 是指向該函數的指針。這些函數的數組以 function_id 設置為 0 來終止。

Provider 模塊可以鏈接或者不鏈接到 libcrypto,如果沒有鏈接,則它將無法直接訪問任何 libcrypto 函數,所有與 libcrypto 的基本通信將通過 Core 提供的回調函數進行。重要的是,由特定 Provider 分配的內存應由相同的 Provider 來釋放,同樣,libcrypto 中分配的內存應由 libcrypto 釋放。

API 將指定一組眾所周知的回調函數編號,在後續發佈中,可以根據需要添加更多的函數編號,而不會破壞向後兼容性。

/* Functions provided by the Core to the provider */
#define OSSL_FUNC_ERR_PUT_ERROR                        1
#define OSSL_FUNC_GET_PARAMS                           2
/* Functions provided by the provider to the Core */
#define OSSL_FUNC_PROVIDER_QUERY_OPERATION             3
#define OSSL_FUNC_PROVIDER_TEARDOWN                    4                  4

Core 將設置一個眾所周知的回調函數數組:

static OSSL_DISPATCH core_callbacks[] = {
    { OSSL_FUNC_ERR_PUT_ERROR, ERR_put_error },
    /* int ossl_get_params(OSSL_PROVIDER *prov, OSSL_PARAM params[]); */
    { OSSL_FUNC_GET_PARAMS, ossl_get_params, }
    /* ... and more */
};

這只是核心可能決定傳遞給 Provider 的一些函數之一。根據需要,我們還可以傳遞用於日誌記錄、測試、儀表等方面的函數。

一旦模塊加載完成並找到了眾所周知的入口點,Core 就可以調用初始化入口點:

/*
 * NOTE: this code is meant as a simple demonstration of what could happen
 * in the core.  This is an area where the OSSL_PROVIDER type is not opaque.
 */
OSSL_PROVIDER *provider = OSSL_PROVIDER_new();
const OSSL_DISPATCH *provider_callbacks;
/*
 * The following are diverse parameters that the provider can get the values
 * of with ossl_get_params.
 */
/* reference to the loaded module, or NULL if built in */
provider->module = dso;
/* reference to the path of the loaded module */
provider->module_path = dso_path;
/* reference to the NCONF structure used for this provider */
provider->conf_module = conf_module;

if (!OSSL_provider_init(provider, core_callbacks, &provider_callbacks))
    goto err;

/* populate |provider| with functions passed by the provider */
while (provider_callbacks->func_num > 0) {
    switch (provider_callbacks->func_num) {
    case OSSL_FUNC_PROVIDER_QUERY_OPERATION:
        provider->query_operation = provider_callbacks->func;
        break;
    case OSSL_FUNC_PROVIDER_TEARDOWN:
        provider->teardown = provider_callbacks->func;
        break;
    }
    provider_callbacks++;
}

OSSL_provider_init 入口點不會註冊任何需要的算法,但它將返回至少這兩個回調函數以啓用這個過程:

OSSL_FUNC_QUERY_OPERATION,用於查找可用的操作實現。它必須返回一個 OSSL_ALGORITHM 數組 (見下文) ,將算法名稱和屬性定義字符串映射到實現調度表,該函數還必須能夠指示結果數組是否可以被 Core 緩存,下面將詳細解釋這一點。

OSSL_FUNC_TEARDOWN,在 Provider 被卸載時使用。

Provider 註冊回調只能在 OSSL_provider_init() 調用成功後執行。

Provider 初始化和算法註冊

一個算法提供一組操作 (功能、特性等) ,這些操作通過函數調用,例如,RSA 算法提供簽名和加密 (兩個操作) ,通過 initupdatefinal 函數進行簽名,以及 initupdatefinal 函數進行加密,函數集由上層 EVP 代碼的實現確定。

操作通過唯一的編號進行標識,例如:

#define OSSL_OP_DIGEST                     1
#define OSSL_OP_SYM_ENCRYPT                2
#define OSSL_OP_SEAL                       3
#define OSSL_OP_DIGEST_SIGN                4
#define OSSL_OP_SIGN                       5
#define OSSL_OP_ASYM_KEYGEN                6
#define OSSL_OP_ASYM_PARAMGEN              7
#define OSSL_OP_ASYM_ENCRYPT               8
#define OSSL_OP_ASYM_SIGN                  9
#define OSSL_OP_ASYM_DERIVE               10

要使 Provider 中的算法可供 libcrypto 使用,它必須註冊一個操作查詢回調函數,該函數根據操作標識返回一個實現描述符數組:

<算法名稱,屬性定義字符串,實現的 OSSL_DISPATCH* >

因此,例如,如果給定的操作是 OSSL_OP_DIGEST,此查詢回調將返回其所有摘要的列表。

算法通過字符串進行標識。

Core 庫以函數表的形式提供了一組服務供 Provider 使用。

Provider 還將通過提供的回調函數提供返回信息的服務 (以附錄-參數傳遞中指定的參數形式) ,例如:

  • 版本號
  • 構建字符串 - 根據當前 OpenSSL 相關的構建信息 (僅在 Provider 級別)
  • Provider 名稱

為了實現一個操作,可能需要定義多個函數回調,每個函數將通過數字函數標識進行標識,對於操作和函數的組合,每個標識都是唯一的,即為摘要操作的 init 函數分配的編號不能用於其他操作的 init 函數,它們將有自己的唯一編號。例如,對於摘要操作,需要以下這些函數:

#define OSSL_OP_DIGEST_NEWCTX_FUNC         1
#define OSSL_OP_DIGEST_INIT_FUNC           2
#define OSSL_OP_DIGEST_UPDATE_FUNC         3
#define OSSL_OP_DIGEST_FINAL_FUNC          4
#define OSSL_OP_DIGEST_FREECTX_FUNC        5
typedef void *(*OSSL_OP_digest_newctx_fn)(void *provctx);
typedef int (*OSSL_OP_digest_init_fn)(void *ctx);
typedef int (*OSSL_OP_digest_update_fn)(void *ctx, void *data, size_t len);
typedef int (*OSSL_OP_digest_final_fn)(void *ctx, void *md, size_t mdsize,
                                       size_t *outlen);
typedef void (*OSSL_OP_digest_freectx_fn)(void *ctx);

對於無法處理多部分操作的設備,還建議使用多合一版本:

#define OSSL_OP_DIGEST_FUNC                6
typedef int (*OSSL_OP_digest)(void *provctx,
                              const void *data, size_t len,
                              unsigned char *md, size_t mdsize,
                              size_t *outlen);

然後,Provider 定義包含每個算法實現的函數集的數組,併為每個操作定義一個算法描述符數組,算法描述符在前面提到過,並且可以公開定義如下:

typedef struct ossl_algorithm_st {
    const char *name;
    const char *properties;
    OSSL_DISPATCH *impl;
} OSSL_ALGORITHM;

例如 (這只是一個示例,Provider 可以按照自己的方式組織這些內容,重要的是算法查詢函數(如下面的 fips_query_operation返回的內容)) ,FIPS 模塊可以定義如下數組來表示 SHA1 算法:

static OSSL_DISPATCH fips_sha1_callbacks[] = {
    { OSSL_OP_DIGEST_NEWCTX_FUNC, fips_sha1_newctx },
    { OSSL_OP_DIGEST_INIT_FUNC, fips_sha1_init },
    { OSSL_OP_DIGEST_UPDATE_FUNC, fips_sha1_update },
    { OSSL_OP_DIGEST_FINAL_FUNC, fips_sha1_final },
    { OSSL_OP_DIGEST_FUNC, fips_sha1_digest },
    { OSSL_OP_DIGEST_FREECTX_FUNC, fips_sha1_freectx },
    { 0, NULL }
};
static const char prop_fips[] = "fips";
static const OSSL_ALGORITHM fips_digests[] = {
    { "sha1", prop_fips, fips_sha1_callbacks },
    { "SHA-1", prop_fips, fips_sha1_callbacks }, /* alias for "sha1" */
    { NULL, NULL, NULL }
};

FIPS Provider 初始化模塊入口點函數可能如下所示:

static int fips_query_operation(void *provctx, int op_id,
                                const OSSL_ALGORITHM **map)
{
    *map = NULL;
    switch (op_id) {
    case OSSL_OP_DIGEST:
        *map = fips_digests;
        break;
    }
    return *map != NULL;
}

#define param_set_string(o,s) do {                                  \
    (o)->buffer = (s);                                              \
    (o)->data_type = OSSL_PARAM_UTF8_STRING_PTR;                    \
    if ((o)->result_size != NULL) *(o)->result_size = sizeof(s);    \
} while(0)
static int fips_get_params(void *provctx, OSSL_PARAM *outparams)
{
    while (outparams->key != NULL) {
        if (strcmp(outparams->key, "provider.name") == 0) {
            param_set_string(outparams, "OPENSSL_FIPS");
        } else if if (strcmp(outparams->key, "provider.build") == 0) {
            param_set_string(outparams, OSSL_FIPS_PROV_BUILD_STRING);
        }
    }
    return 1;
}

OSSL_DISPATCH provider_dispatch[] = {
    { OSSL_FUNC_PROVIDER_QUERY_OPERATION, fips_query_operation },
    { OSSL_FUNC_PROVIDER_GET_PARAMS, fips_get_params },
    { OSSL_FUNC_PROVIDER_STATUS, fips_get_status },
    { OSSL_FUNC_PROVIDER_TEARDOWN, fips_teardown },
    { 0, NULL }
};
static core_put_error_fn *core_put_error = NULL;
static core_get_params_fn *core_get_params = NULL;

int OSSL_provider_init(const OSSL_PROVIDER *provider,
                       const OSSL_DISPATCH *in,
                       const OSSL_DISPATCH **out
                       void **provider_ctx)
{
    int ret = 0;

    /*
     * Start with collecting the functions provided by the core
     * (we could write it more elegantly, but ...)
     */
    while (in->func_num > 0) {
        switch (in->func_num) {
        case OSSL_FUNC_ERR_PUT_ERROR:
            core_put_error = in->func;
            break;
        case OSSL_FUNC_GET_PARAMS:
            core_get_params = in->func;
            Break;
        }
        in++;
    }

    /* Get all parameters required for self tests */
    {
        /*
         * All these parameters come from a configuration saying this:
         *
         * [provider]
         * selftest_i = 4
         * selftest_path = "foo"
         * selftest_bool = true
         * selftest_name = "bar"
         */
        OSSL_PARAM selftest_params[] = {
            { "provider.selftest_i", OSSL_PARAM_NUMBER,
              &selftest_i, sizeof(selftest_i), NULL },
            { "provider.selftest_path", OSSL_PARAM_STRING,
              &selftest_path, sizeof(selftest_path), &selftest_path_ln },
            { "provider.selftest_bool", OSSL_PARAM_BOOLEAN,
              &selftest_bool, sizeof(selftest_bool), NULL },
            { "provider.selftest_name", OSSL_PARAM_STRING,
              &selftest_name, sizeof(selftest_name), &selftest_name_ln },
            { NULL, 0, NULL, 0, NULL }
        }
        core_get_params(provider, selftest_params);
    }

    /* Perform the FIPS self test - only return params if it succeeds. */
    if (OSSL_FIPS_self_test()) {
        *out = provider_dispatch;
        return 1;
    }
    return 0;
}

算法選擇

同時可能存在多個 Provider,重新編譯為此版本的現有應用程序代碼應該可以繼續工作。與此同時,通過進行輕微的代碼調整,應該能夠使用基於屬性的新算法查找功能來查找和使用算法。

為了説明這個過程是如何工作的,下面的代碼是使用 OpenSSL 1.1.1 進行簡單的 AES-CBC-128 加密的示例。為簡潔起見,所有的錯誤處理都已被剝離。

EVP_CIPHER_CTX *ctx;
EVP_CIPHER *ciph;

ctx = EVP_CIPHER_CTX_new();
ciph = EVP_aes_128_cbc();
EVP_EncryptInit_ex(ctx, ciph, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &clen, plaintext, plen);
EVP_EncryptFinal_ex(ctx, ciphertext + clen, &clentmp);
clen += clentmp;

EVP_CIPHER_CTX_free(ctx);

在 OpenSSL 3.0 中,這樣的代碼仍然可以正常工作,並且將使用來自 Provider 的算法 (假設沒有進行其他配置,將使用默認 Provider) ,它也可以通過顯式獲取進行重寫,如下所示。顯式獲取還可以使應用程序在需要時指定非默認的庫上下文 (在此示例中為 osslctx

EVP_CIPHER_CTX *ctx;
EVP_CIPHER *ciph;

ctx = EVP_CIPHER_CTX_new();
ciph = EVP_CIPHER_fetch(osslctx, "aes-128-cbc", NULL);                /* <=== */
EVP_EncryptInit_ex(ctx, ciph, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &clen, plaintext, plen);
EVP_EncryptFinal_ex(ctx, ciphertext + clen, &clentmp);
clen += clentmp;

EVP_CIPHER_CTX_free(ctx);
EVP_CIPHER_free(ciph);                                                /* <=== */

應用程序可能希望使用來自不同 Provider 的算法。

例如,考慮這樣的情況:應用程序希望使用 FIPS Provider 的某些算法,但在某些情況下仍然使用默認算法。可以以不同的方式實現,例如:

  • 只使用 FIPS 算法
  • 默認使用 FIPS 算法,但能夠在需要時進行覆蓋,以獲得對非 FIPS 算法的訪問
  • 默認不關心 FIPS 算法,但能夠在需要時進行覆蓋,以獲得 FIPS 算法

只使用 FIPS 算法

與 OpenSSL 3.0.0 之前版本編寫的代碼相比,如果您只需要 FIPS 實現,則只需要像這樣進行一些更改:

int main(void)
{
    EVP_set_default_alg_properties(NULL, "fips=yes");                 /* <=== */
    ...
}

然後,使用 EVP_aes_128_cbc() 的上述加密代碼將繼續像以前一樣工作,EVP_EncryptInit_ex() 調用將使用默認的算法屬性,並通過 Core 查找以獲取與 FIPS 實現關聯的句柄,然後,該實現將與 EVP_CIPHER_CTX 對象關聯起來,如果沒有適用的算法實現可用,EVP_Encrypt_init_ex() 調用將失敗。

EVP_set_default_alg_properties 的第一個參數是庫上下文,NULL 表示默認的內部上下文。

默認使用 FIPS 算法,但允許覆蓋

要將默認設置為使用 FIPS 算法,但根據需要覆蓋為非 FIPS 算法,與 pre-3.0.0 OpenSSL 的代碼相比,應用程序可能會進行以下更改:

int main(void)
{
    EVP_set_default_alg_properties(osslctx, "fips=yes");              /* <=== */
    ...
}

EVP_CIPHER_CTX *ctx;
EVP_CIPHER *ciph;

ctx = EVP_CIPHER_CTX_new();
ciph = EVP_CIPHER_fetch(osslctx, "aes-128-cbc", "fips!=yes");         /* <=== */
EVP_EncryptInit_ex(ctx, ciph, NULL, key, iv);
EVP_EncryptUpdate(ctx, ciphertext, &clen, plaintext, plen);
EVP_EncryptFinal_ex(ctx, ciphertext + clen, &clentmp);
clen += clentmp;

EVP_CIPHER_CTX_free(ctx);
EVP_CIPHER_free(ciph);                                                /* <=== */

這裏的 EVP_CIPHER_fetch() 調用會結合以下屬性:

  • 默認的算法屬性
  • 作為參數傳入的屬性 (傳入的屬性優先級更高)

因為 EVP_CIPHER_fetch() 調用覆蓋了默認的 fips 屬性,它將尋找一個不是 fips 的 AES-CBC-128 的實現。

在這個例子中,我們看到使用了非默認的庫上下文,這隻有在明確獲取實現的情況下才可能發生。 (注意:對於細心的讀者, fips!=yes 也可以寫為 fips=no ,但這裏提供的是“不等於”運算符的一個示例)

默認不關注 FIPS 算法,並允許覆蓋 FIPS

為了默認不使用 FIPS 算法,但可以根據需要覆蓋為使用 FIPS 算法,應用程序代碼可能如下所示 (與 3.0.0 之前版本的 OpenSSL 代碼相比)

EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(privkey, NULL);
EVP_ASYM *asym = EVP_ASYM_fetch(osslctx, EVP_PKEY_EC, "fips=yes");
EVP_PKEY_CTX_set_alg(pctx, asym));
EVP_PKEY_derive_init(pctx);
EVP_PKEY_derive_set_peer(pctx, pubkey);
EVP_PKEY_derive(pctx, out, &outlen);
EVP_PKEY_CTX_free(pctx);

在這個版本中,我們沒有在 main 中覆蓋默認的算法屬性,因此你將獲得默認的開箱即用設置,即不要求使用 FIPS 算法。然而,我們在 EVP_CIPHER_fetch() 級別上明確設置了 fips 屬性,因此它覆蓋了默認設置。當 EVP_CIPHER_fetch() 使用 Core 查找算法時,它將獲得對 FIPS 算法的引用;如果沒有這樣的算法,則失敗。

非對稱算法選擇

請注意,對於對稱加密/解密和消息摘要,存在現有的 OpenSSL 對象可用於表示算法,即 EVP_CIPHER 和 EVP_MD。對於非對稱算法,沒有等效的對象,使用的算法從 EVP_PKEY 的類型隱式推斷出來。

為了解決這個問題,將引入一個新的非對稱算法對象。在下面的示例中,執行了一個 ECDH 密鑰派生操作,我們使用一個新的算法對象 EVP_ASYM 來查找 FIPS 的 ECDH 實現 (需要假設我們知道給定的私鑰是 ECC 私鑰)

EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new(privkey, NULL);
EVP_ASYM *asym = EVP_ASYM_fetch(osslctx, EVP_PKEY_EC, "fips=yes");
EVP_PKEY_CTX_set_alg(pctx, asym));
EVP_PKEY_derive_init(pctx);
EVP_PKEY_derive_set_peer(pctx, pubkey);
EVP_PKEY_derive(pctx, out, &outlen);
EVP_PKEY_CTX_free(pctx);

算法選擇動態視圖示例

下面的時序圖展示瞭如何從默認 Provider 中選擇和調用 SHA256 算法的示例。

圖片

請注意,EVP 層的每個調用都由 EVP 層中的薄封裝器實現,這些封裝器按照算法的方式在 Provider 中調用同名函數,要使用的特定 Provider 函數將通過顯式的 EVP_MD_fetch() 調用在 Core 調度表中查找,該調用指定了消息摘要名稱作為字符串以及其他相關屬性,返回的 "md" 對象包含對所選 Provider 中算法實現的函數指針。

EVP_MD_CTX 對象沒有傳遞給 Provider,因為我們不知道任何特定的 Provider 模塊是否與 libcrypto 鏈接,相反,我們只是傳遞一個黑盒句柄 (void指針)* ,Provider 將與其所需的任何結構相關聯。在操作開始時,通過對 Provider 進行明確的 digestNewCtx() 調用來分配此句柄,並在結束時通過 digestFreeCtx() 調用來釋放。

下一個圖示展示了稍微複雜一些的情景,即使用 RSA 和 SHA256 的 EVP_DigestSign* 操作。該圖示從 libcrypto 的角度繪製,其中算法由 FIPS 模塊提供,稍後的章節將從 FIPS 模塊的角度考察這個情景。

圖片

EVP_DigestSign* 操作更加複雜,因為它涉及兩個算法:簽名算法和摘要算法。通常情況下,這兩個算法可能來自不同的 Provider,也可能來自同一個 Provider。在使用 FIPS 模塊的情況下,這兩個算法必須來自同一個 FIPS 模塊 Provider,如果嘗試違反這個規則,操作將失敗。

儘管有兩個算法的額外複雜性,但與之前圖示中展示的簡單的 EVP_Digest* 操作相同的概念仍然適用。生成了兩個上下文:EVP_MD_CTX 和 EVP_PKEY_CTX。這兩個上下文都不會傳遞給 Provider。相反,通過顯式的 "newCtx" Provider 調用創建黑盒 (void )* 句柄,然後在後續的 initupdate 和 final 操作中傳遞這些句柄。

算法是通過提前使用顯式的 EVP_MD_fetch() 和 EVP_ASYM_fetch() 調用在 Core 調度表中查找的。

下週我們將帶來 FIPS 模塊 部分內容,等不及的小夥伴,可以查看銅鎖語雀中的全篇文檔哦!
https://www.yuque.com/tsdoc/ts/openssl-300-design#CckIP

user avatar lafengdehuanghuacai 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.