前一章介绍了 cmake 的基本语法 以及如何构建一个最简单的工程。这里接着聊一聊使用 cmake 构建工程的常用操作:添加库
一、由源代码添加库
这一节中我们将向工程中添加一个库项目。
假设我们需要开发一个 mathlib
库, 并在其他项目中调用,可以像下面这样操作:
1. 在工程路径下建立子文件夹 mathlib
2. 在 ./mathlib
中添加项目源文件 mymath.h
, mymath.cxx
,并添加 CMakeLists.txt
:
前一章介绍了 cmake 的基本语法 以及如何构建一个最简单的工程。这里接着聊一聊使用 cmake 构建工程的常用操作:添加库
这一节中我们将向工程中添加一个库项目。
假设我们需要开发一个 mathlib
库, 并在其他项目中调用,可以像下面这样操作:
1. 在工程路径下建立子文件夹 mathlib
2. 在 ./mathlib
中添加项目源文件 mymath.h
, mymath.cxx
,并添加 CMakeLists.txt
:
cmake 文件包括 CMakeLists.txt
和以 .cmake
为后缀的文件。
程序源文件最外层的 CMakeLists.txt 文件是 cmake 的入口文件,这个文件可以定义了整个工程的构建规范。它也可以使用 add_subdirectory()
命令来包含一个子文件夹,每个使用该命令添加的子文件夹中也需要有一个 CMakeLists.txt 文件。
在 cmake 中, 变量都是以字符串的形式存在的。使用 set( )
包装变量, 使用 ${}
展开变量。${}
可以嵌套使用。 使用未包装的变量会导致空展开:
1 2 3 4 5 6 7 8 9 10 |
set(var_name_1 var1) message("var_name is ${var_name_1}") set(var_name_2 var2) set(${var_name_2} var) #same as set(var2 var) set(${${var_name_2}}_x foo) #same as set(var_x foo) message("var2 is ${var2}") message("var_name_2 is ${var_name_2}") message("var_x is ${var_x}") message("empty var is ${empty_var}") |
执行结果如下:
1 2 3 4 5 |
var_name is var1 var2 is var var_name_2 is var2 var_x is foo empty var is |
当我们在构建工程时,在不同的平台上使用不同的工具,如在Windows上使用 Visual Studio 工程, 在 Mac 上使用 Xcode 工程。
这些工程只限于特定的平台。当我们需要跨平台构建工程时,就需要使用 CMake
了,它是一个开源的 跨平台系统构建工具
。同一个 CMake 工程可以在不同的平台上转换为与平台适应的工具,极大的方便了跨平台工程构建。
新建一个空目录,在其中新建一个文本文件,命名为 CMakeLists.txt
,并输入以下内容:
1 |
message("Hello,This is my first cmake project") |
好了,你的第一个 cmake 工程就已经建好了。接下来,新建一个文件夹 build
, 并在该文件夹下运行命令:
1 |
cmake .. |
你可能会得到以下输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
-- Building for: Visual Studio 15 2017 -- The C compiler identification is MSVC 19.15.26732.1 -- The CXX compiler identification is MSVC 19.15.26732.1 -- Check for working C compiler: ....../cl.exe -- Check for working C compiler: ....../cl.exe -- works -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working CXX compiler: ....../cl.exe -- Check for working CXX compiler: ....../cl.exe -- works -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Detecting CXX compile features -- Detecting CXX compile features - done Hello,This is my first cmake project -- Configuring done -- Generating done -- Build files have been written to: ....../build |
再来看这个路径下,cmake 生成了一堆文件:
我们成功的使用 cmake 在 Windows 上构建了一个 vs 工程。 当然,如果你的操作系统是 MacOS, 或者 Linux, 结果后有所不同。
其实在 CMakeLists.txt 所以在路径下也能运行 cmake 命令。不过cmake没有提供专门的工具来清理生成的文件,为了方便管理,我们将其生成在 build 目录下。
这个 cmake 工程的作用仅仅是输出一了条消息,并没有什么意义。下面我们来让它更有意义一点:
在之前一篇博客 ECC(Elliptic Curves Cryptography) 椭圆曲线加密原理 简单地阐述了 ECC 加解密的原理。这本篇博客中接着来聊一聊如何使用 OPENSSL 来进行 ECC 加解密。
首先需要明确一点的是:ECC 本身并没有定义一套加解密的方法,它主要作用于密钥交换(ECDHE),与签名认证(ECDSA). 不过后来中国工程师设计定义了一套加密方法,并于近年得到了世界的认可,这就是中国商用(国家标准) SM2 椭圆曲线公钥密码算法
。
其实在 openssl 中,椭圆曲线分两种形式,一种是之前讲到的质数域上的椭圆曲线 ,将其称为 $F_p$ 其方程为:
$$y^2 \ \mod \ p = x^3 + ax + b \ \mod \ p$$
另一种是二进制域,称为 $F_{2^m}$, 其方程为:
$$y^2 + xy = x^3 + ax^2 + b ,\ (b != 0)$$
在这里只讨论 $F_p$ 相关的内容。
椭圆曲线上的点使用 EC_POINT
来表示, 它定义在 ec_locl.h
:
1 2 3 4 5 6 7 |
struct ec_point_st { // ... BIGNUM *X; BIGNUM *Y; BIGNUM *Z; // ... }; |
在我之前的博客中聊到了 DH 密钥交换的原理 . 这里来讨论如何使用 OPENSSL 进行 DH 密钥交换。
OPENSSL 提供了一系列API, 可以在方便地进行大数计算。在这里我们先对 DH 密钥交换的原理 中讲到的原理进行代码级别的验证。对原理不感兴趣的可以跳过这一小节。
由于在 DH 密钥交换过程中,大部分的操作过程都是一样的,可以定义一个基类来抽象:
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 |
class DHBase { public: DHBase() : m_pri_key(NULL) {} virtual ~DHBase() { if (m_pri_key) BN_free(m_pri_key); }; const BIGNUM *GetSharedNum() { if (!m_pri_key) GenRandomData(); BN_CTX *ctx = BN_CTX_new(); const BIGNUM *g = DH_get0_g(m_dh); const BIGNUM *p = DH_get0_p(m_dh); BIGNUM *r = BN_new(); // r = g ^ m_pri_key % p int rst = BN_mod_exp(r, g, m_pri_key, p, ctx); assert(rst == 1); BN_CTX_free(ctx); return r; } BIGNUM *GenKey(const BIGNUM *shared_num) { if (!m_pri_key) GenRandomData(); BIGNUM *key = BN_new(); BN_CTX *ctx = BN_CTX_new(); // key = shared_num ^ m_pri_key % p int rst = BN_mod_exp(key, shared_num, m_pri_key, DH_get0_p(m_dh), ctx); assert(rst == 1); BN_CTX_free(ctx); std::cout << m_name << ": received shared num: " << GetNumString(shared_num) << std::endl; std::cout << m_name << ": generated KEY: " << GetNumString(key) << std::endl; return key; } std::string GetName() { return m_name; } static std::string GetNumString(const BIGNUM *num) { return BN_bn2hex(num); } private: void GenRandomData() { assert(m_dh); m_pri_key = BN_new(); int rst = BN_rand_range(m_pri_key, DH_get0_p(m_dh)); assert(rst == 1); while (BN_is_zero(m_pri_key)) { BN_rand_range(m_pri_key, DH_get0_p(m_dh)); } std::cout << m_name << ": generated the private num" << std::endl; } protected: DH *m_dh; std::string m_name; private: BIGNUM *m_pri_key; }; |
m_pri_key
是双方生成的私密随机数。GetSharedNum()
返回双方交换的中间变量 G^A mod P
GenKey()
即完成了密钥交换,返回交换成功后的密钥。在我之前的一篇博客中 RSA 公钥加密原理 中, 对 RSA 非对称加密原理做了简单的阐述。这篇博客主要聊如何使用 OPENSSL 进行密钥对的生成,以及非对称加解密。
在 OPENSSL 中, RSA
是一个很重要的结构体。它的定义在 rsa_locl.h
中,面包含了在原理中提到的所有重要的变量 随机质数 p
, q
, 公钥指数 e
, 私钥指数 d
, 以及模数 n
1 2 3 4 5 6 7 8 9 |
struct rsa_st { // ... BIGNUM *n; BIGNUM *e; BIGNUM *d; BIGNUM *p; BIGNUM *q; // ... }; |
生成密钥函数:
1 |
int RSA_generate_key_ex(RSA *rsa, int bits, BIGNUM *e, BN_GENCB *cb); |
bits
密钥的规模(modulus
)。小于 1028 位的密钥是不安全的,小于 512 则会返回 0e
公开的指数。它应该是一个奇数(odd number), 一般是 3, 17
或 65537
cb
生成大随机数的回调函数。一般使用 NULL 即可, 默认为 BN_GENCB_call()
在我之前的一篇博客里介绍了 对称加密的模式 . 这里主要聊一聊如何使用 openssl
来进行 AES 加密
.
openssl 加密 API 分两个部分: High Level
and Low Level
. 对于大部分人来说,使用 High Level 就够用了, 这些 API 被冠以 EVP
(Envelope) ,表示对 Low Level 的封装。High Level API 提供了包括 对称/非对称加解密
, 签名
, 验证
, 哈希
,MAC
等一系列组件,屏蔽了 Low Level API 的复杂逻辑,使用起来安全高效。对于除非有需要进行加密算法级别的改进,否则不建议使用 Low Level API.
大部分 EVP API 有一个 int 型返回值 ,用来表示操作是否成功:1 表示成功, 0 表示失败。但有些时候也会返回 -1 ,表达如内存分配或者其他什么错误。官方指导代码如下:
1 2 3 4 5 6 7 |
if(1 != EVP_xxx()) goto err; if(1 != EVP_yyy()) goto err; /* ... do some stuff ... */ err: ERR_print_errors_fp(stderr); |
对于 AES 加解密,EVP API 分为两种,EVP_Encrypt / EVP_Decryp
系列 和 EVP_Cipher
系列。后者是对前者进一步的封装。它们具体使用的套路都是一样的:
EVP_CIPHER_CTX
xxxInit()
函数,使用 key(密钥)、iv(前置向量)
和 cipher(算法)
对上下文初始化xxxUpdate()
函数进行加解密。该函数支持流式操作。即对于一段明文来说,分成多组按顺序进行加密,和一次性全部加密,不影响其生成的密文的正确性。xxxFinal()
函数获取上下文中遗留的信息和对称加密不同,公钥加密(非对称加密)的密钥分为加密密钥(公钥)和解密密钥(私钥)。 公钥是公开的,任何人都有可能知道公钥,并用公钥生成密文,而私钥是保密的,只有解密者才能知道私钥,用它来解密密文获得明文。在这里对使用最广泛的公钥密码算法-- RSA.
RSA 是发明此算法的三位科学家的姓氏的首字母(吐槽一下:外国人的命名真随意)
在RSA中,明文,密钥和密文都是数字, RSA的加密过程可以用如下公式表达:
$$密文 = 明文^E \mod N$$
即: RSA 的密文是明文的 $E$ 次方求模 $N$ 的结果, 或者说,密文是明文的 $E$ 次方除以 $N$ 的余数。这就是整个加密过程,它非常简洁。因此,如果知道了 $E$ 和 $N$ ,那么任何人都可以进行加密运算,也就是说,
$$ E 和 N 是公钥$$
在实际使用中, E 和 N 是经精心计算的数字。
RSA 解密公式如下:
$$ 明文= 密文^D \mod N$$
即,对密文的 $D$ 次方求模 $N$ 运算,就可以得到明文。这里是
$$D 和 N 是私钥 $$