OpenSSL是坑货写的

jopen 10年前

(注* 通过Heartbleed这个致使漏洞,任何人都可远程访问运行了OpenSSL服务器的内存,参见: heart bleed 其实早在09年就有人喷过OpenSSL)

 

我知道这听起来很刺耳,但让我们调查一下事实吧。

首先,让我们避免别人抱怨的方式在自己身上发生。嗨,我是Marco Peereboom,我写开源代码是兴趣所在。我已经参与过一些项目了,谷歌会告诉你我参与了哪些项目。我不是一个伟大的程序员,但我离此也不远,我已经写了一些东西。

最近我参与编写一些与通信安全有关的代码,我认为除了用众所周知的被广大用户使用的库以外没有更好的办法了。基本上我的问题归结为以下几点:

  • 写一个提供CA(认证授权)认证服务的程序。
  • 将所有的证书存储在一个LDAP树上。

我们讨论的不是关于这个想法的优点,但这是我们旅途的开始。这听起来简单极了,因此去网络上找相应的文档和代码段,这简直就像是在公园里散步,因为所有的这些问题以前就有人解决过了。

和这些代码倒腾了近一个月之后,我决定详细记载一下这些内容,希望可以省一些精力。我已经得出结论了,OpenSSL就像猴子在墙上泼粪一样。它是我用过最烂的库,没有之一。我简直不敢相信互联网居然运行在这样一个荒唐复杂且莫名愚蠢的代码之上的。自1998年以来几乎全世界都认为他们的安全通信是建立在这个坚不可摧的自称为“OpenSSL”的烂项目上的。我敢打赌,在这个问题上,医生也开不出任何处方。

第一天:

根 本没法找到完整的代码。我的寻找对CA问题一点用也没有。我找到的东西基本上就是一大堆“怎么做”,但他们几乎都没有提到最关键的问题。他们是好意的,但 只提供方法而没有实质的东西确实伤害到了我。好吧,让我们去Barnes & Nobles(美国最大的实体书店)找找看吧,看我们能找到些什么 东西。

坑啊!什么都没有,真的。我找到两本书,两本书都是基本上都是关于使用命令行工具的。其中一本书上有一些范例代码,但真没有什么值得借鉴的地方。显然,人们似乎只关心使用openssl这个“工具”而已。

我看了一下代码,但这些代码完全把我给搞晕了,所以我还是选择离开了。不管怎么说,现在我开始怀疑这东西可能没有我想象中那么简单。不管怎样还是先回家喝杯冷饮吧。

第二天: 

好吧,管它的!我还是要看下那些讨厌的代码;我指的是网络上使用的所有书本和范例。至少它能给我指出来我需要用到哪些函数,然后我就可以在更好的文档上查找这些函数。首先我显然需要找出使用openssl这个“工具”的办法。

8小时之后.......

嗬,阅读了无数个“怎么做”之后我找到了一些有用的东西。那份文档写的都是人们为何这么说?真的很差劲!让我来分享一下我用爱做出的成果吧。我创建了3个脚本来举例说明我用CA签署的客户端和服务器证书的问题。

1.create_ca,这个脚本创建了CA。

2.create_server,这个脚本创建了服务器证书和密钥。

3.create_client,这个脚本创建了客户端证书和密钥。

create_ca

#/bin/sh  mkdir -p ca/private  chmod 700 ca/private  openssl req -x509 -days 3650 -newkey rsa:1024 -keyout ca/private/ca.key -out ca/ca.crt

create_server

#/bin/sh  mkdir -p server/private  chmod 700 server/private  openssl genrsa -out server/private/server.key 1024  openssl req -new -key server/private/server.key -out server/server.csr

create_client

#/bin/sh  mkdir -p client/private  chmod 700 client/private  openssl genrsa -out client/private/client.key 1024  openssl req -new -key client/private/client.key -out client/client.csr

今天做的已经够了。该回家了。

第三天:

好吧,openssl“工具”这个东西让我们偏离了深挖那段代码的路线。额,主函数在哪?它怎么调用那些单一的模块。噢,用大小写加下划线啊。这个主宏太棒了,*WASH_Eyes_outWith_soap*

好 了,现在我们需要一些重要的标签以便来操作这些混乱的代码。通过几小时的深挖代码之后,我找到了窍门(感谢vim!)。现在是时候开始重新组织这些代码 了,看看我不用openssl这个“工具”能不能生成CA。又几个小时过去了,不完整的操作手册,没有操作手册,写得很差的操作手册这些东西确实开始困扰 着我了。没有谷歌,必应,雅虎这些搜索引擎的帮助。完全没有文档且就算有文档,里面的内容都是过时的而且没有实际联系的。管它的,我该回家了!

第四天:

我开始写一些基于我在openssl里找到的东西的代码。由于令人尴尬的不良风格,代码的压缩且#ifdef阻止我开始,我的进度极其缓慢。说到#ifdef,我看到一些能吞掉指令的不稳定的东西。例如:

#ifdef (OMG)   if (moo) {    ...   } else  #endif /* OMG */    yeah();

实际上,这已经是比较不错的版本了。我用的版本是经过压缩的几百行代码构成的,这能把男子汉给搞哭。同样地让我们以不同寻常的方式得到另外的一些东西。如果你认为

if (moo)      {      dome_something_dumb();      }      else      {      or_not();      }  or  if (   moo)  {  blah();  }  if (bad)  goto err;  ...  if (0) {  err:  do_something_horrible();  }

可读性好,那么我建议你还是去检测下视力吧。甚至可能找一下直肠科医师。让我们来看一些真实的案例:

 if ((OBJ_obj2nid(obj) == NID_pkcs9_emailAddress) &&     (str->type != V_ASN1_IA5STRING))     {     BIO_printf(bio_err,"\nemailAddress type needs to be of type IA5STRING\n");     goto err;     }    if ((str->type != V_ASN1_BMPSTRING) && (str->type != V_ASN1_UTF8STRING))     {     j=ASN1_PRINTABLE_type(str->data,str->length);     if ( ((j == V_ASN1_T61STRING) &&       (str->type != V_ASN1_T61STRING)) ||      ((j == V_ASN1_IA5STRING) &&       (str->type == V_ASN1_PRINTABLESTRING)))      {      BIO_printf(bio_err,"\nThe string contains characters that are illegal for the ASN.1 type\n");      goto err;      }     }

下面是一个函数堆栈的例子。

 if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt", SSL_FILETYPE_PEM))  ctrl ]  ...   else if (type == SSL_FILETYPE_PEM)    {    j=ERR_R_PEM_LIB;    x=PEM_read_bio_X509(in,NULL,ctx->default_passwd_callback,ctx->default_passwd_callback_userdata);  ctrl ]  ...  #define PEM_read_bio_X509(bp,x,cb,u) (X509 *)PEM_ASN1_read_bio( \   (char *(*)())d2i_X509,PEM_STRING_X509,bp,(char **)x,cb,u)  ctrl ]  ...   if (!PEM_bytes_read_bio(&data, &len, NULL, name, bp, cb, u))    return NULL;  ctrl ]  ...    if (!PEM_read_bio(bp,&nm,&header,&data,&len)) {  ctrl ]  ...    i=BIO_gets(bp,buf,254);  ctrl ]  ...   i=b->method->bgets(b,in,inl);

它跨过了5个文件,6个间接文件,都是为了打开文件 fget到文件的内容。我们仍然在使用间接调用。当所有我想要的是一个能将PEM(不在一个文件中) cert翻译成 X509结构的函数时,所有这些工 作和障碍都存在着。但百万左右的函数不方便那样存在。我多少有点怀疑,但由于没有文档,我真的只能猜测了。我也不能从这个gem把你们抢走:

#ifndef OPENSSL_NO_STDIO  /*!   * Load CA certs from a file into a ::STACK. Note that it is somewhat misnamed;   * it doesn't really have anything to do with clients (except that a common use   * for a stack of CAs is to send it to the client). Actually, it doesn't have   * much to do with CAs, either, since it will load any old cert.   * \param file the file containing one or more certs.   * \return a ::STACK containing the certs.   */  STACK_OF(X509_NAME) *SSL_load_client_CA_file(const char *file)   {   BIO *in;   X509 *x=NULL;   X509_NAME *xn=NULL;   STACK_OF(X509_NAME) *ret = NULL,*sk;     sk=sk_X509_NAME_new(xname_cmp);     in=BIO_new(BIO_s_file_internal());     if ((sk == NULL) || (in == NULL))    {    SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE);    goto err;    }      if (!BIO_read_filename(in,file))    goto err;     for (;;)    {    if (PEM_read_bio_X509(in,&x,NULL,NULL) == NULL)     break;    if (ret == NULL)     {     ret = sk_X509_NAME_new_null();     if (ret == NULL)      {      SSLerr(SSL_F_SSL_LOAD_CLIENT_CA_FILE,ERR_R_MALLOC_FAILURE);      goto err;      }     }    if ((xn=X509_get_subject_name(x)) == NULL) goto err;    /* check for duplicates */    xn=X509_NAME_dup(xn);    if (xn == NULL) goto err;    if (sk_X509_NAME_find(sk,xn) >= 0)     X509_NAME_free(xn);    else     {     sk_X509_NAME_push(sk,xn);     sk_X509_NAME_push(ret,xn);     }    }     if (0)    {  err:    if (ret != NULL) sk_X509_NAME_pop_free(ret,X509_NAME_free);    ret=NULL;    }   if (sk != NULL) sk_X509_NAME_free(sk);   if (in != NULL) BIO_free(in);   if (x != NULL) X509_free(x);   if (ret != NULL)    ERR_clear_error();   return(ret);   }  #endif

哇!所有的东西都放在里面实现了!不可读,对于函数如何实现来说,它有太多的间接调用和不清晰的直接调用。不得不喜欢if (0)这个结构!我指的是显然它赢得了所有的关于美的奖。你必须要很高兴地知道这个风格的代码支撑起我们许多的“安全”网络。

我真傻,只想需要一个函数从LDAP或内存读取证书以便我能自己写LDAP的代码。没有写太多的代码,我需要回家喝上两杯。

第五天:

它开始让我反感了!伴随着各种头疼的东西,我开始着手代码部分了。经过几个小时的阅读和反复阅读openssl这个“工具”的代码,我想出了这个:

int  create_ca(char *retstr, size_t retlen)  {   int   rv = 1;   int   days = 365 * 10;   char   *password = NULL;   EVP_PKEY  pkey, *tmppkey = NULL;   BIGNUM   bn;   RSA   *rsa = NULL;   X509_REQ  *req = NULL;   X509_NAME  *subj;   X509   *x509 = NULL;   BIO   *out = NULL;     /* generate private key */   if ((rsa = RSA_new()) == NULL)    ERROR_OUT(ERR_SSL, done);   bzero(&bn, sizeof bn);   if (BN_set_word(&bn, 0x10001) == 0)    ERROR_OUT(ERR_SSL, done);   if (RSA_generate_key_ex(rsa, 1024, &bn, NULL) == 0)    ERROR_OUT(ERR_SSL, done);   bzero(&pkey, sizeof pkey);   if (EVP_PKEY_assign_RSA(&pkey, rsa) == 0)    ERROR_OUT(ERR_SSL, done);     /* setup req for certificate */   if ((req = X509_REQ_new()) == NULL)    ERROR_OUT(ERR_SSL, done);   if (X509_REQ_set_version(req, 0) == 0)    ERROR_OUT(ERR_SSL, done);   subj = X509_REQ_get_subject_name(req);   if (validate_canew(subj, &password)) {    snprintf(last_error, sizeof last_error,        "validate_canew failed");    ERROR_OUT(ERR_OWN, done);   }   /* set public key to req */   if (X509_REQ_set_pubkey(req, &pkey) == 0)    ERROR_OUT(ERR_SSL, done);     /* generate 509 cert */   if ((x509 = X509_new()) == NULL)    ERROR_OUT(ERR_SSL, done);   bzero(&bn, sizeof bn);   if (BN_pseudo_rand(&bn, 64 /* bits */, 0, 0) == 0)    ERROR_OUT(ERR_SSL, done);   if (BN_to_ASN1_INTEGER(&bn, X509_get_serialNumber(x509)) == 0)    ERROR_OUT(ERR_SSL, done);   if (X509_set_issuer_name(x509, X509_REQ_get_subject_name(req)) == 0)    ERROR_OUT(ERR_SSL, done);   if (X509_gmtime_adj(X509_get_notBefore(x509), 0) == 0)    ERROR_OUT(ERR_SSL, done);   if (days == 0) {    snprintf(last_error, sizeof last_error,        "not enough days for certificate");    ERROR_OUT(ERR_OWN, done);   }   days *= 60 * 60 * 24;   if (X509_gmtime_adj(X509_get_notAfter(x509), days) == 0)    ERROR_OUT(ERR_SSL, done);   if (X509_set_subject_name(x509, X509_REQ_get_subject_name(req)) == 0)    ERROR_OUT(ERR_SSL, done);   if ((tmppkey = X509_REQ_get_pubkey(req)) == NULL)    ERROR_OUT(ERR_SSL, done);   if (X509_set_pubkey(x509, tmppkey) == 0)    ERROR_OUT(ERR_SSL, done);   if (X509_sign(x509, &pkey, EVP_sha1()) == 0)    ERROR_OUT(ERR_SSL, done);     /* write private key */   out = BIO_new(BIO_s_file());   if (BIO_write_filename(out, CA_PKEY) <= 0)    ERROR_OUT(ERR_SSL, done);   if (chmod(CA_PKEY, S_IRWXU))    ERROR_OUT(ERR_LIBC, done);   if (PEM_write_bio_PrivateKey(out, &pkey, EVP_des_ede3_cbc(), NULL, 0,       NULL, password) == 0)    ERROR_OUT(ERR_SSL, done);   BIO_free_all(out);     /* write cert */   out = BIO_new(BIO_s_file());   if (BIO_write_filename(out, CA_CERT) <= 0)    ERROR_OUT(ERR_SSL, done);   if (PEM_write_bio_X509(out, x509) == 0)    ERROR_OUT(ERR_SSL, done);   BIO_free_all(out);     rv = 0;  done:   if (tmppkey)    EVP_PKEY_free(tmppkey);   if (x509)    X509_free(x509);   if (req)    X509_REQ_free(req);   if (rsa)    RSA_free(rsa);     return (rv);   }

正如你能看到的,我创建了一个可怕的机制,至少它能得到一些可用的错误堆栈来追踪错误。这面就是这个令人可怕的宏:

/* errors */  #define ERR_LIBC (0)  #define ERR_SSL  (1)  #define ERR_OWN  (2)    #define ERROR_OUT(e, g) do { push_error(__FILE__, __FUNCTION__, __LINE__, e); goto g; } while(0)

 亲爱的 $DEITY ,我请求你原谅我犯的罪。

让我来展示一下其余的内容来完成这项工作,且希望我能帮助那些忍受这东西而丢了魂的人。函数与这个废物一同工作:

 

char *  geterror(int et)  {   char   *es;     switch (et) {   case ERR_LIBC:    strlcpy(last_error, strerror(errno), sizeof last_error);    break;   case ERR_SSL:    es = (char *)ERR_lib_error_string(ERR_get_error());    if (es)     strlcpy(last_error, es, sizeof last_error);    else     strlcpy(last_error, "unknown SSL error",         sizeof last_error);    break;   default:    strlcpy(last_error, "unknown error", sizeof last_error);    /* FALLTHROUGH */   case ERR_OWN:    break;   }     return (last_error);  }    void  push_error(char *file, char *func, int line, int et)  {   struct error *ce;     if ((ce = calloc(1, sizeof *ce)) == NULL)    fatal("push_error ce");   if ((ce->file = strdup(file)) == NULL)    fatal("push_error ce->file");   if ((ce->func = strdup(func)) == NULL)    fatal("push_error ce->func");   if ((ce->errstr = strdup(geterror(et))) == NULL)    fatal("push_error ce->errstr");   ce->line = line;     SLIST_INSERT_HEAD(&ces, ce, dlink);  }

下面是能让它完全“工作”的剩下的实用功能:

int  cert_find_put(char *entry, X509_NAME *subj, ssize_t min, ssize_t max)  {   struct valnode  *v;   int   rv = 1;     v = find_valtree(entry);   if (v && v->length > 0) {    if (min != -1 && v->length < min) {     snprintf(last_error, sizeof last_error,         "%s minimum constraint not met %lu < %lu",         entry, v->length, min);     ERROR_OUT(ERR_OWN, done);    }    if (max != -1 && v->length > max) {     snprintf(last_error, sizeof last_error,         "%s maximum constraint not met %lu > %lu",         entry, v->length, max);     ERROR_OUT(ERR_OWN, done);    }    if (X509_NAME_add_entry_by_txt(subj, entry, MBSTRING_ASC,        v->value, -1, -1, 0) == 0)     ERROR_OUT(ERR_SSL, done);   } else {    log_debug("cert_find_put: %s not found", entry);    goto done;   }     rv = 0;  done:   return (rv);  }    int  validate_canew(X509_NAME *subj, char **pwd)  {   struct valnode  *password, *password2;   int   rv = 1;     password = find_valtree("password");   password2 = find_valtree("password2");     if (password && password2) {    if (strcmp(password->value, password2->value) ||        password->length == 0) {     snprintf(last_error, sizeof last_error,         "invalid password");     ERROR_OUT(ERR_OWN, done);    }    *pwd = password->value;   }   if (password == NULL && password2 == NULL) {    snprintf(last_error, sizeof last_error,        "password can't be NULL");    ERROR_OUT(ERR_OWN, done);   }     if (cert_find_put("C", subj, 2, 2)) {    snprintf(last_error, sizeof last_error,        "invalid country");    ERROR_OUT(ERR_OWN, done);   }   cert_find_put("ST", subj, -1, -1);   cert_find_put("L", subj, -1, -1);   cert_find_put("O", subj, -1, -1);   cert_find_put("OU", subj, -1, -1);   cert_find_put("CN", subj, -1, -1);   cert_find_put("emailAddress", subj, -1, -1);     rv = 0;  done:   return (rv);  }

庆祝一下!我的天呐,我们有CA了。哈哈,该回家了!

回到家,我洗了个凉且哭了,等等,那是血吗???

第六天:

我过后会写一些LDAP的东西。我暂时需要处理别的东西。所以接下来,需要一个协商SSL或TLS的客户端或服务器应用。刚开始,我测试了网上各种各样的例子。

第一次测试失败

好吧,我们需要看看操作手册,我认为它们都会有例子的。

第二次失败

我在网络,例子,和许多时间中找到的东西都不奏效。

第三次失败

够了,我要回家了。

第七天:

好吧,是时候回到目前为止我最爱的代码段上了。Openssl这个“工具”有S_server和s_client且如果你点动按钮,他们似乎能工作。下面这些是我想出来的神奇命令:

openssl s_server -CAfile ca/ca.crt -cert server/server.crt -key server/private/server.key -Verify 1  openssl s_client -CAfile ca/ca.crt -cert client/client.crt -key client/private/client.key

越过SSL或TLS的连接是通过它实现的,而且它似乎对tcpdump也同样有效。所以让我们开始敲代码吧!我将在这给你介绍我的代码,再次地,我希望给其它小伙伴们开一个好的头,而且希望能避免一些歧途。首先建立服务器:

#include <stdio.h>  #include <stdlib.h>  #include <err.h>    #include <sys/types.h>  #include <sys/socket.h>    #include <netinet/in.h>    #include "openssl/bio.h"  #include "openssl/ssl.h"  #include "openssl/err.h"    void  fatalx(char *s)  {   ERR_print_errors_fp(stderr);   errx(1, s);  }    int  main(int argc, char *argv[])  {   SSL_CTX   *ctx;   BIO   *sbio;   SSL   *ssl;   int   sock, s, r, val = -1;   struct sockaddr_in sin;     SSL_load_error_strings();   OpenSSL_add_ssl_algorithms();     ctx = SSL_CTX_new(SSLv23_server_method());   if (ctx == NULL)    fatalx("ctx");     if (!SSL_CTX_load_verify_locations(ctx, "ca/ca.crt", NULL))    fatalx("verify");   SSL_CTX_set_client_CA_list(ctx, SSL_load_client_CA_file("ca/ca.crt"));     if (!SSL_CTX_use_certificate_file(ctx, "server/server.crt", SSL_FILETYPE_PEM))    fatalx("cert");   if (!SSL_CTX_use_PrivateKey_file(ctx, "server/private/server.key", SSL_FILETYPE_PEM))    fatalx("key");   if (!SSL_CTX_check_private_key(ctx))    fatalx("cert/key");     SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);   SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);   SSL_CTX_set_verify_depth(ctx, 1);     /* setup socket */   if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)    err(1, "socket");     bzero(&sin, sizeof sin);   sin.sin_addr.s_addr = INADDR_ANY;   sin.sin_family = AF_INET;   sin.sin_port = htons(4433);   setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof val);     if (bind(sock, (struct sockaddr *)&sin, sizeof sin) == -1)    err(1, "bind");   listen(sock, 0);     for (;;) {    if ((s = accept(sock, 0, 0)) == -1)     err(1, "accept");      sbio = BIO_new_socket(s, BIO_NOCLOSE);    ssl = SSL_new(ctx);    SSL_set_bio(ssl, sbio, sbio);      if ((r = SSL_accept(ssl)) == -1)     fatalx("SSL_accept");      printf("handle it!\n");   }     return (0);  }

接下来是客户端:

#include <stdio.h>  #include <stdlib.h>  #include <err.h>    #include <sys/types.h>  #include <sys/socket.h>    #include <netinet/in.h>    #include "openssl/bio.h"  #include "openssl/ssl.h"  #include "openssl/err.h"    void  fatalx(char *s)  {   ERR_print_errors_fp(stderr);   errx(1, s);  }    int  main(int argc, char *argv[])  {   SSL_CTX   *ctx;   BIO   *sbio;   SSL   *ssl;   struct sockaddr_in addr;   struct hostent  *hp;   int   sock;       SSL_load_error_strings();   OpenSSL_add_ssl_algorithms();     ctx = SSL_CTX_new(SSLv23_client_method());   if (ctx == NULL)    fatalx("ctx");     if (!SSL_CTX_load_verify_locations(ctx, "ca/ca.crt", NULL))    fatalx("verify");     if (!SSL_CTX_use_certificate_file(ctx, "client/client.crt", SSL_FILETYPE_PEM))    fatalx("cert");   if (!SSL_CTX_use_PrivateKey_file(ctx, "client/private/client.key", SSL_FILETYPE_PEM))    fatalx("key");   if (!SSL_CTX_check_private_key(ctx))    fatalx("cert/key");     SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);   SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);   SSL_CTX_set_verify_depth(ctx, 1);     /* setup connection */   if ((hp = gethostbyname("localhost")) == NULL)    err(1, "gethostbyname");     bzero(&addr, sizeof addr);   addr.sin_addr = *(struct in_addr *)hp->h_addr_list[0];   addr.sin_family = AF_INET;   addr.sin_port = htons(4433);     if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)    err(1, "socket");   if (connect(sock, (struct sockaddr *)&addr, sizeof addr) == -1)    err(1, "connect");     /* go do ssl magic */   ssl = SSL_new(ctx);   sbio = BIO_new_socket(sock, BIO_NOCLOSE);   SSL_set_bio(ssl, sbio, sbio);     if (SSL_connect(ssl) <= 0)    fatalx("SSL_connect");     if (SSL_get_verify_result(ssl) != X509_V_OK)    fatalx("cert");     return (0);  }

这不是我最好的状态,但它还是能行的,且一些人可能会受益于此。回家!

第八天:

多看一些关于如何让这些可恶的文件在LDAP内工作的东西。在会议和其它一些很逊的事之间,我放弃了且写了这篇文章。随着功能的完善,我会继续更新的。我应该克服OpenSSL这个满身臭味的家伙,做出自己的来。

来自: ourjs.com