现在的位置: 首页 > 云计算 > 正文

OpenSSL 入门:密码学基础知识

2020年02月07日 云计算 ⁄ 共 8863字 ⁄ 字号 评论关闭

  OpenSSL 是在 Linux 和其他系统上流行的生产级库和工具包。OpenSSL 实用程序可在命令行使用,程序也可以调用 OpenSSL 库中的函数。本文的示例程序使用的是 C 语言,即 OpenSSL 库的源语言。


  OpenSSL 简史


  安全套接字层(Secure Socket Layer)(SSL)是 Netscape 在 1995 年发布的一种加密协议。该协议层可以位于 HTTP 之上,从而为 HTTPS 提供了 S: 安全(secure)。SSL 协议提供了各种安全服务,其中包括两项在 HTTPS 中至关重要的服务:


  对等身份验证(Peer authentication)(也称为相互质询):连接的每一边都对另一边的身份进行身份验证。如果 Alice 和 Bob 要通过 SSL 交换消息,则每个人首先验证彼此的身份。


  机密性(Confidentiality):发送者在通过通道发送消息之前先对其进行加密。然后,接收者解密每个接收到的消息。此过程可保护网络对话。即使窃听者 Eve 截获了从 Alice 到 Bob 的加密消息(即中间人攻击),Eve 会发现他无法在计算上解密此消息。


  反过来,这两个关键 SSL 服务与其他不太受关注的服务相关联。例如,SSL 支持消息完整性,从而确保接收到的消息与发送的消息相同。此功能是通过哈希函数实现的,哈希函数也随 OpenSSL 工具箱一起提供。


  SSL 有多个版本(例如 SSLv2 和 SSLv3),并且在 1999 年出现了一个基于 SSLv3 的类似协议 传输层安全性(Transport Layer Security)(TLS)。TLSv1 和 SSLv3 相似,但不足以相互配合工作。不过,通常将 SSL/TLS 称为同一协议。例如,即使正在使用的是 TLS(而非 SSL),OpenSSL 函数也经常在名称中包含 SSL。此外,调用 OpenSSL 命令行实用程序以 openssl 开始。


  除了 man 页面之外,OpenSSL 的文档是零零散散的,鉴于 OpenSSL 工具包很大,这些页面很难以查找使用。命令行和代码示例可以将主要主题集中起来。让我们从一个熟悉的示例开始(使用 HTTPS 访问网站),然后使用该示例来选出我们感兴趣的加密部分进行讲述。


  一个 HTTPS 客户端


  此处显示的 client 程序通过 HTTPS 连接到 Google:


  /* compilation: gcc -o client client.c -lssl -lcrypto */


  #include


  #include


  #include /* BasicInput/Output streams */


  #include /* errors */


  #include /* core library */


  #define BuffSize 1024


  void report_and_exit(const char* msg) {


  perror(msg);


  ERR_print_errors_fp(stderr);


  exit(-1);


  }


  void init_ssl() {


  SSL_load_error_strings();


  SSL_library_init();


  }


  void cleanup(SSL_CTX* ctx, BIO* bio) {


  SSL_CTX_free(ctx);


  BIO_free_all(bio);


  }


  void secure_connect(const char* hostname) {


  char name[BuffSize];


  char request[BuffSize];


  char response[BuffSize];


  const SSL_METHOD* method = TLSv1_2_client_method();


  if (NULL == method) report_and_exit("TLSv1_2_client_method...");


  SSL_CTX* ctx = SSL_CTX_new(method);


  if (NULL == ctx) report_and_exit("SSL_CTX_new...");


  BIO* bio = BIO_new_ssl_connect(ctx);


  if (NULL == bio) report_and_exit("BIO_new_ssl_connect...");


  SSL* ssl = NULL;


  /* 链路 bio 通道,SSL 会话和服务器端点 */


  sprintf(name, "%s:%s", hostname, "https");


  BIO_get_ssl(bio, &ssl); /* 会话 */


  SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 鲁棒性 */


  BIO_set_conn_hostname(bio, name); /* 准备连接 */


  /* 尝试连接 */


  if (BIO_do_connect(bio) <= 0) {


  cleanup(ctx, bio);


  report_and_exit("BIO_do_connect...");


  }


  /* 验证信任库,检查证书 */


  if (!SSL_CTX_load_verify_locations(ctx,


  "/etc/ssl/certs/ca-certificates.crt", /* 信任库 */


  "/etc/ssl/certs/")) /* 其它信任库 */


  report_and_exit("SSL_CTX_load_verify_locations...");


  long verify_flag = SSL_get_verify_result(ssl);


  if (verify_flag != X509_V_OK)


  fprintf(stderr,


  "##### Certificate verification error (%i) but continuing...\n",


  (int) verify_flag);


  /* 获取主页作为示例数据 */


  sprintf(request,


  "GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A",


  hostname);


  BIO_puts(bio, request);


  /* 从服务器读取 HTTP 响应并打印到输出 */


  while (1) {


  memset(response, '\0', sizeof(response));


  int n = BIO_read(bio, response, BuffSize);


  if (n <= 0) break; /* 0 代表流结束,< 0 代表有错误 */


  puts(response);


  }


  cleanup(ctx, bio);


  }


  int main() {


  init_ssl();


  const char* hostname = "www.google.com:443";


  fprintf(stderr, "Trying an HTTPS connection to %s...\n", hostname);


  secure_connect(hostname);


  return 0;


  }


  可以从命令行编译和执行该程序(请注意 -lssl 和 -lcrypto 中的小写字母 L):


  gcc -o client client.c -lssl -lcrypto


  该程序尝试打开与谷歌网站的安全连接。在与 Google Web 服务器的 TLS 握手过程中,client 程序会收到一个或多个数字证书,该程序会尝试对其进行验证(但在我的系统上失败了)。尽管如此,client 程序仍继续通过安全通道获取 Google 主页。该程序取决于前面提到的安全工件,尽管在上述代码中只着重突出了数字证书。但其它工件仍在幕后发挥作用,稍后将对它们进行详细说明。


  通常,打开 HTTP(非安全)通道的 C 或 C++ 的客户端程序将使用诸如文件描述符或网络套接字之类的结构,它们是两个进程(例如,这个 client 程序和 Google Web 服务器)之间连接的端点。另一方面,文件描述符是一个非负整数值,用于在程序中标识该程序打开的任何文件类的结构。这样的程序还将使用一种结构来指定有关 Web 服务器地址的详细信息。


  这些相对较低级别的结构不会出现在客户端程序中,因为 OpenSSL 库会将套接字基础设施和地址规范等封装在更高层面的安全结构中。其结果是一个简单的 API。下面首先看一下 client 程序示例中的安全性详细信息。


  该程序首先加载相关的 OpenSSL 库,我的函数 init_ssl 中对 OpenSSL 进行了两次调用:


  SSL_load_error_strings();


  SSL_library_init();


  下一个初始化步骤尝试获取安全上下文,这是建立和维护通往 Web 服务器的安全通道所需的信息框架。如对 OpenSSL 库函数的调用所示,在示例中使用了 TLS 1.2:


  const SSL_METHOD* method = TLSv1_2_client_method(); /* TLS 1.2 */


  如果调用成功,则将 method 指针被传递给库函数,该函数创建类型为 SSL_CTX 的上下文:


  SSL_CTX* ctx = SSL_CTX_new(method);


  client 程序会检查每个关键的库调用的错误,如果其中一个调用失败,则程序终止。


  现在还有另外两个 OpenSSL 工件也在发挥作用:SSL 类型的安全会话,从头到尾管理安全连接;以及类型为 BIO( 基本输入/输出(Basic Input/Output))的安全流,用于与 Web 服务器进行通信。BIO 流是通过以下调用生成的:


  BIO* bio = BIO_new_ssl_connect(ctx);


  请注意,这个最重要的上下文是其参数。BIO 类型是 C 语言中 FILE 类型的 OpenSSL 封装器。此封装器可保护 client 程序与 Google 的网络服务器之间的输入和输出流的安全。


  有了 SSL_CTX 和 BIO,然后程序在 SSL 会话中将它们组合在一起。三个库调用可以完成工作:


  BIO_get_ssl(bio, &ssl); /* 会话 */


  SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* 鲁棒性 */


  BIO_set_conn_hostname(bio, name); /* 准备连接 */


  安全连接本身是通过以下调用建立的:


  BIO_do_connect(bio);


  如果最后一个调用不成功,则 client 程序终止;否则,该连接已准备就绪,可以支持 client 程序与 Google Web 服务器之间的机密对话。


  在与 Web 服务器握手期间,client 程序会接收一个或多个数字证书,以认证服务器的身份。但是,client 程序不会发送自己的证书,这意味着这个身份验证是单向的。(Web 服务器通常配置为不需要客户端证书)尽管对 Web 服务器证书的验证失败,但 client 程序仍通过了连接到 Web 服务器的安全通道继续获取 Google 主页。


  为什么验证 Google 证书的尝试会失败?典型的 OpenSSL 安装目录为 /etc/ssl/certs,其中包含 ca-certificates.crt 文件。该目录和文件包含着 OpenSSL 自带的数字证书,以此构成 信任库(truststore)。可以根据需要更新信任库,尤其是可以包括新信任的证书,并删除不再受信任的证书。


  client 程序从 Google Web 服务器收到了三个证书,但是我的计算机上的 OpenSSL 信任库并不包含完全匹配的证书。如目前所写,client 程序不会通过例如验证 Google 证书上的数字签名(一个用来证明该证书的签名)来解决此问题。如果该签名是受信任的,则包含该签名的证书也应受信任。尽管如此,client 程序仍继续获取页面,然后打印出 Google 的主页。下一节将更详细地介绍这些。


  客户端程序中隐藏的安全性


  让我们从客户端示例中可见的安全工件(数字证书)开始,然后考虑其他安全工件如何与之相关。数字证书的主要格式标准是 X509,生产级的证书由诸如 Verisign 的 证书颁发机构(Certificate Authority)(CA)颁发。


  数字证书中包含各种信息(例如,激活日期和失效日期以及所有者的域名),也包括发行者的身份和数字签名(这是加密过的加密哈希值)。证书还具有未加密的哈希值,用作其标识指纹。


  哈希值来自将任意数量的二进制位映射到固定长度的摘要。这些位代表什么(会计报告、小说或数字电影)无关紧要。例如, 消息摘要版本 5(Message Digest version 5)(MD5)哈希算法将任意长度的输入位映射到 128 位哈希值,而 SHA1( 安全哈希算法版本 1(Secure Hash Algorithm version 1))算法将输入位映射到 160 位哈希值。不同的输入位会导致不同的(实际上在统计学上是唯一的)哈希值。下一篇文章将会进行更详细的介绍,并着重介绍什么使哈希函数具有加密功能。


  数字证书的类型有所不同(例如根证书、中间证书和最终实体证书),并形成了反映这些证书类型的层次结构。顾名思义,根证书位于层次结构的顶部,其下的证书继承了根证书所具有的信任。OpenSSL 库和大多数现代编程语言都具有 X509 数据类型以及处理此类证书的函数。来自 Google 的证书具有 X509 格式,client 程序会检查该证书是否为 X509_V_OK。


  X509 证书基于 公共密钥基础结构(public-key infrastructure)(PKI),其中包括的算法(RSA 是占主导地位的算法)用于生成密钥对:公共密钥及其配对的私有密钥。公钥是一种身份:Amazon 的公钥对其进行标识,而我的公钥对我进行标识。私钥应由其所有者负责保密。


  成对出现的密钥具有标准用途。可以使用公钥对消息进行加密,然后可以使用同一个密钥对中的私钥对消息进行解密。私钥也可以用于对文档或其他电子工件(例如程序或电子邮件)进行签名,然后可以使用该对密钥中的公钥来验证签名。以下两个示例补充了一些细节。


  在第一个示例中,Alice 将她的公钥分发给全世界,包括 Bob。然后,Bob 用 Alice 的公钥加密邮件,然后将加密的邮件发送给 Alice。用 Alice 的公钥加密的邮件将可以用她的私钥解密(假设是她自己的私钥),如下所示:


  +------------------+ encrypted msg +-------------------+


  Bob's msg--->|Alice's public key|--------------->|Alice's private key|---> Bob's msg


  +------------------+ +-------------------+


  理论上可以在没有 Alice 的私钥的情况下解密消息,但在实际情况中,如果使用像 RSA 这样的加密密钥对系统,则在计算上做不到。


  现在,第二个示例,请对文档签名以证明其真实性。签名算法使用密钥对中的私钥来处理要签名的文档的加密哈希:


  +-------------------+


  Hash of document--->|Alice's private key|--->Alice's digital signature of the document


  +-------------------+


  假设 Alice 以数字方式签署了发送给 Bob 的合同。然后,Bob 可以使用 Alice 密钥对中的公钥来验证签名:


  +------------------+


  Alice's digital signature of the document--->|Alice's public key|--->verified or not


  +------------------+


  假若没有 Alice 的私钥,就无法轻松伪造 Alice 的签名:因此,Alice 有必要保密她的私钥。


  在 client 程序中,除了数字证书以外,这些安全性都没有明确展示。下一篇文章使用使用 OpenSSL 实用程序和库函数的示例填充更多详细的信息。


  命令行的 OpenSSL


  同时,让我们看一下 OpenSSL 命令行实用程序:特别是在 TLS 握手期间检查来自 Web 服务器的证书的实用程序。调用 OpenSSL 实用程序可以使用 openssl 命令,然后添加参数和标志的组合以指定所需的操作。


  看看以下命令:


  openssl list-cipher-algorithms


  该输出是组成 加密算法套件(cipher suite)()的相关算法的列表。下面是列表的开头,加了澄清首字母缩写词的注释:


  AES-128-CBC ## Advanced Encryption Standard, Cipher Block Chaining


  AES-128-CBC-HMAC-SHA1 ## Hash-based Message Authentication Code with SHA1 hashes


  AES-128-CBC-HMAC-SHA256 ## ditto, but SHA256 rather than SHA1


  ...


  下一条命令使用参数 s_client 将打开到谷歌的安全连接,并在屏幕上显示有关此连接的所有信息:


  openssl s_client -connect www.google.com:443 -showcerts


  端口号 443 是 Web 服务器用于接收 HTTPS(而不是 HTTP 连接)的标准端口号。(对于 HTTP,标准端口为 80)Web 地址 www.google.com:443 也出现在 client 程序的代码中。如果尝试连接成功,则将显示来自 Google 的三个数字证书以及有关安全会话、正在使用的加密算法套件以及相关项目的信息。例如,这是开头的部分输出,它声明证书链即将到来。证书的编码为 base64:


  Certificate chain


  0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=www.google.com


  i:/C=US/O=Google Trust Services/CN=Google Internet Authority G3


  -----BEGIN CERTIFICATE-----


  MIIEijCCA3KgAwIBAgIQdCea9tmy/T6rK/dDD1isujANBgkqhkiG9w0BAQsFADBU


  MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw


  ...


  诸如 Google 之类的主要网站通常会发送多个证书进行身份验证。


  输出以有关 TLS 会话的摘要信息结尾,包括加密算法套件的详细信息:


  SSL-Session:


  Protocol : TLSv1.2


  Cipher : ECDHE-RSA-AES128-GCM-SHA256


  Session-ID: A2BBF0E4991E6BBBC318774EEE37CFCB23095CC7640FFC752448D07C7F438573


  ...


  client 程序中使用了协议 TLS 1.2,Session-ID 唯一地标识了 openssl 实用程序和 Google Web 服务器之间的连接。Cipher 条目可以按以下方式进行解析:


  ECDHE( 椭圆曲线 Diffie-Hellman(临时)(Elliptic Curve Diffie Hellman Ephemeral))是一种用于管理 TLS 握手的高效的有效算法。尤其是,ECDHE 通过确保连接双方(例如,client 程序和 Google Web 服务器)使用相同的加密/解密密钥(称为会话密钥)来解决“密钥分发问题”。后续文章会深入探讨该细节。


  RSA(Rivest Shamir Adleman)是主要的公共密钥密码系统,并以 1970 年代末首次描述了该系统的三位学者的名字命名。这个正在使用的密钥对是使用 RSA 算法生成的。


  AES128( 高级加密标准(Advanced Encryption Standard))是一种 块式加密算法(block cipher),用于加密和解密 位块(blocks of bits)。(另一种算法是 流式加密算法(stream cipher),它一次加密和解密一个位。)这个加密算法是对称加密算法,因为使用同一个密钥进行加密和解密,这首先引起了密钥分发问题。AES 支持 128(此处使用)、192 和 256 位的密钥大小:密钥越大,安全性越好。


  通常,像 AES 这样的对称加密系统的密钥大小要小于像 RSA 这样的非对称(基于密钥对)系统的密钥大小。例如,1024 位 RSA 密钥相对较小,而 256 位密钥则当前是 AES 最大的密钥。


  GCM( 伽罗瓦计数器模式(Galois Counter Mode))处理在安全对话期间重复应用的加密算法(在这种情况下为 AES128)。AES128 块的大小仅为 128 位,安全对话很可能包含从一侧到另一侧的多个 AES128 块。GCM 非常有效,通常与 AES128 搭配使用。


  SHA256( 256 位安全哈希算法(Secure Hash Algorithm 256 bits))是我们正在使用的加密哈希算法。生成的哈希值的大小为 256 位,尽管使用 SHA 甚至可以更大。


  加密算法套件正在不断发展中。例如,不久前,Google 使用 RC4 流加密算法(RSA 的 Ron Rivest 后来开发的 Ron’s Cipher 版本 4)。 RC4 现在有已知的漏洞,这大概部分导致了 Google 转换为 AES128。


  总结


  我们通过安全的 C Web 客户端和各种命令行示例对 OpenSSL 做了首次了解,使一些需要进一步阐明的主题脱颖而出。

抱歉!评论已关闭.