【问题标题】:TLS handshake on EAPOL 802.1XEAPOL 802.1X 上的 TLS 握手
【发布时间】:2022-06-30 23:29:23
【问题描述】:

我正在构建一个 EAP-TLS 身份验证客户端 (802.1X EAPOL)。到目前为止的要求只是 EAP-TLS。我正在使用 FreeRadius 服务器进行测试,它使用的是 TLS 1.1,所以这是我正在开发的传输版本。

因为这个请求者使用的是自定义的网络堆栈,并且在小型嵌入式设备上,我不能使用 OpenSSL 库,因为它们将所有握手作为黑盒套接字级别进行通信。此外,我发现的所有请求者都包含与 AAA 和 Authenticator 紧密交织的代码。我没有那么多空间来添加所有源(除了使支持更加困难)

不管怎样,一边学习一边自己动手学习是件好事。

所以,当我深入研究时,我看到的东西与 RFC 不一致或根本没有定义。

在向 WPA-Supplicant 邮寄有关尝试“自己动手”的问题之前,我首先想礼貌地问一个简单的问题:“这是提出技术问题的好地方,还是有其他资源”。我被礼貌地忽略了。所以我在这里发帖。

参考RFC 3579、3748、4346、5216等,我对服务器进行了MD5质询认证。成功理解 EAP、以太网数据包、片段等。

在 TLS 上,我已成功接收、组装和解析 TLS Server Hello 握手。 (RFC 5216 仅定义了 EAP 上的 TLS 标头,而 RFC 4346 解释了完整的 TLS 握手,但 EAP 使用了其中的一个子集。)由于我可以访问测试服务器证书和密钥,因此我还验证了使用公钥,它用私钥正确解密。

现在我正在尝试逐步构建完整的客户端握手,向消息中添加块。并找到我无法解决的问题。

下面,我指的是 RFC 4346 以获取以下 TLS 1.1 消息。

在第 4.3 节中,向量是用特定的“表示语言”定义的。使用 [] 表示固定的已知长度,使用 <..> 表示必须包含指示大小的前导值的变量长度。

第 7.4.7 节定义了客户端密钥交换。就我而言,它只是一个 RSA,因此是一个“EncryptedPreMasterSecret”。 7.4.7.1节定义了RSA的EncryptedPreMasterSecret,即版本号和随机数,共48个字节。

该定义并未声明这是一个可变向量。然而,如果来自 FreeRadius 的调试信息没有长度为两字节的主机顺序值,则它会拒绝它。


(27) eap_tls:   TLS-Client-Cert-X509v3-Basic-Constraints += "CA:FALSE"
(27) eap_tls: TLS_accept: SSLv3/TLS read client certificate
(27) eap_tls: <<< recv TLS 1.0 Handshake [length 0104], ClientKeyExchange
(27) eap_tls: >>> send TLS 1.0 Alert [length 0002], fatal decode_error
(27) eap_tls: ERROR: TLS Alert write:fatal:decode error
tls: TLS_accept: Error in error
(27) eap_tls: ERROR: Failed in __FUNCTION__ (SSL_read): error:1419F09F:SSL routines:tls_process_cke_rsa:length mismatch
(27) eap_tls: ERROR: System call (I/O) error (-1)
(27) eap_tls: ERROR: TLS receive handshake failed during operation
(27) eap_tls: ERROR: [eaptls process] = fail

有趣的是,Wireshark 似乎并不介意它是否丢失。

通过添加两个字节长度,我克服了这个失败。但是,我不喜欢它不符合我阅读的规范。

这是在我遗漏的其他地方描述的吗?

所以我似乎已经通过了 PremasterSecret,并继续处理证书验证消息。至于它,第 7.4.8 节定义了包含 MD5 和 SHA 哈希的证书验证,请参阅第 7.4.3 节。 7.4.3 中的定义定义了“签名”是什么,并没有声称这是一个变量向量。

事实上,第 7.4.3 节非常清楚地表明它是一个已知长度向量(即使用固定长度 [16] 和 [20])。然而,Wireshark 也需要一个两字节的标头,如果不存在则报告错误。

所以我把它加了两个字节头,Wireshark 很高兴。

但这仍然不符合规范。已知的最大长度为 36 字节,适合一个 8 位数字。因此,要求两个字节违反了 4.3 节中的规范:

The length will be in the form of a number consuming as many bytes as required to hold the vector’s specified maximum (ceiling) length.

但是,即使进行了更改,服务器仍在抱怨。


(13) eap_tls:   TLS-Client-Cert-X509v3-Basic-Constraints += "CA:FALSE"
(13) eap_tls: TLS_accept: SSLv3/TLS read client certificate
(13) eap_tls: <<< recv TLS 1.0 Handshake [length 0106], ClientKeyExchange
(13) eap_tls: TLS_accept: SSLv3/TLS read client key exchange
(13) eap_tls: <<< recv TLS 1.0 Handshake [length 002a], CertificateVerify
(13) eap_tls: >>> send TLS 1.0 Alert [length 0002], fatal decrypt_error
(13) eap_tls: ERROR: TLS Alert write:fatal:decrypt error
tls: TLS_accept: Error in error
(13) eap_tls: ERROR: Failed in __FUNCTION__ (SSL_read)
(13) eap_tls: ERROR: error:04091077:rsa routines:int_rsa_verify:wrong signature length
(13) eap_tls: ERROR: error:1417B07B:SSL routines:tls_process_cert_verify:bad signature

服务器说“decrypt_error”。此验证消息是否应该加密?规范没有这么说。 Grepping服务器源,我无法在任何地方找到该短信。它被隐藏得很好,很难找到拒绝它的函数。

如果它应该被加密,使用什么密钥?客户端私钥还是服务器公钥?

再说一次,这是否在我遗漏的其他地方进行了描述?它没有在两个方面遵循规范(使用可变长度,两个字节就足够了)。

在第 7.4.9 节中,完成的消息是使用包含“[0..11]”的表示语言定义的,该描述在第 4 节的任何地方都没有定义。它是否是一个错字,意味着是一个可变长度向量 ?或者这里的 [0..11] 是什么意思?

下一个主要问题:

我是不是太难了?

是否有 OpenSSL 调用会简单地接受重新组装的 TLS 握手,并创建客户端握手回复,并将其填充到提供的缓冲区中?同样,因为嵌入式设备上的请求者客户端使用它自己的网络堆栈,所以我不能使用 OpenSSL 的内部套接字调用进行握手。

OpenSSL 文档在许多领域都缺乏,如果存在这样的 API,我也没有偶然发现它。

感谢您的任何回答和建议。

-斯科特

【问题讨论】:

    标签: openssl freeradius tls1.1


    【解决方案1】:

    我已经解决了这个最初的问题,尽管我没有回答所有最初的问题。

    具体来说,我将回答“我是不是太难了”这个问题。答案是是的

    原来我正在寻找的 OpenSSL api 是 BIO api,我了解到它实际上是一个 API,它允许您直接处理缓冲区,而不是使用 OpenSSL 套接字进行通信。通过在发送和接收调用之间插入您的代码,将其视为“中间人”。

    显然 BIO api 也可以用于文件 IO 等其他事情。

    在我的例子中,这是 EAPOL,它是原始以太网数据包,但假设您需要通过不支持 TCP/IP 的 I2C 线路执行一些加密,例如这种 TLS 握手。在这种情况下,您可以将流读入缓冲区,将其复制到输入 BIO 中,然后进行 OpenSSL 调用。然后将数据从输出BIO中取出并发送出去。

    这里有一些可以解释一切的具体代码片段:

    首先,标题。我只是把所有东西都扔了,因为我使用了此处未显示的代码中的许多其他功能(OpenSSL 页面提供了此信息,但没有告诉您原型在哪里的 API 文档是非常 烦我)。一些标题名称是它们贡献的线索:

    #include <openssl/ssl.h>
    #include <openssl/bio.h>
    #include <openssl/md5.h>
    #include <openssl/crypto.h>
    #include <openssl/x509.h>
    #include <openssl/pem.h>
    #include <openssl/x509v3.h>
    #include <openssl/rsa.h>
    #include <openssl/err.h>
    

    下面的这些代码片段被包装在一个类中,并使用前导“m_”的匈牙利符号表示它们是该类的成员。首先,您需要几个 BIO 句柄,一个用于接收和传递给 OpenSSL 的内容,一个用于 OpenSSL 回复的内容。似乎没有任何要求您提前知道缓冲区大小。

    声明 BIOS

    BIO  *m_sslIn, *m_sslOut;
    

    并初始化它们

    m_sslIn = NULL;
    m_sslOut = NULL;
    

    现在假设 SSL 已配置,证书、CA、密钥等都已加载并已创建上下文(此答案并非对 SSL 调用的解释)

    m_sslCtx = SSL_CTX_new(SSLv23_method());
    m_ssl = SSL_new(m_sslCtx);
    

    创建 BIO。 In 和 Out 的创建方式相同。

    m_sslIn = BIO_new(BIO_s_mem());
    BIO_reset(m_sslIn);
    m_sslOut = BIO_new(BIO_s_mem());
    BIO_reset(m_sslOut);
    

    现在是很酷的部分。告诉 OpenSSL 它将使用这些 BIO 作为缓冲区,因此您可以将数据放入和取出。

    SSL_set_bio(m_ssl, m_sslIn, m_sslOut);
    

    这很重要:我发现当你完成后,你不需要调用 BIO_free()。因为 SSL 已连接它们的事实意味着它将为您释放它们。一种令人讨厌的功能,无需告诉任何人。但是在我弄清楚之前,我一直在“释放多个缓冲区”崩溃。

    以下假设您有读取原始数据包或 I2C 线等数据的数据。请注意,当您初始化 TLS 握手时,第一条消息尚不存在(TLS 从您向另一端发送 ClientHello 消息开始)。因此,在第一次调用时将输入 BIO 留空,然后调用 SSL_connect(m_ssl) 开始。

    if (msg) {      //    is there a message?
        result = BIO_write(m_sslIn, msg, len);
    }
    
    result = SSL_connect(m_ssl);
    if (result < 0) {
        int err = SSL_get_error(m_ssl, result);
        printf("Error code: %d\n", err);
        if (err == SSL_ERROR_WANT_READ) {
            printf(" Want More read\n");
        } else if (err == SSL_ERROR_WANT_WRITE) {
            printf(" Want More write\n");
        } else {
            printf(" ERROR\n");
        }
    }
    

    SSL_connect() 调用将失败。但是失败是错误:SSL_ERROR_WANT_READ,这仅仅意味着它正在等待更多的读取。所以,在调用之后,复制 OpenSSL 在输出 BIO 中的内容:

    result = BIO_ctrl_pending(m_sslOut);            //  Any data there to read? 
    if (result == 0) return 0;
    m_msgOut = (unsigned char *) malloc(result);   //  Of course YOU own this memory, and need to clean it up   
    length = BIO_read(m_sslOut,m_msgOut,result);
    m_msgLength = length;
    FlushErrors(m_ssl);        //  See below
    

    在第一次通过时,您将收到开头的“CLIENT_HELLO”消息。

    所以,现在您的缓冲区中有消息,将其发送到服务器,并从服务器读取回复。获取该回复并将其再次存储在输入 BIO 中,再次调用 SSL_connect(),然后从 BIO 中读取响应。一直来回走动,直到握手完成。

    另外,如上面的代码片段所示,每次调用 SSL_connect 时,都必须清除 SSL 句柄中堆积的所有错误:

    int FlushErrors(void *ssl_ctx) {
        int count = 0;
        int result;
        while ((result = ERR_get_error())) {
            printf("TLS - SSL error: %s",
               ERR_error_string(result, NULL));
            count++;
        }
        return count;
    }
    

    对于我的 EAPOL 客户来说,这个过程非常简单。复杂的部分是处理来回消息的碎片,因为一些 TLS 握手可能有很多证书并且非常大。重要的是,在将来自服务器的整个消息重新组装到 BIO 并调用下一个 SSL_connect 之前。

    我希望这可以帮助下一个尝试做同样事情的人。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-06-11
      • 2016-03-21
      • 1970-01-01
      • 2014-12-09
      • 2021-01-02
      • 2017-01-17
      • 1970-01-01
      • 2021-09-12
      相关资源
      最近更新 更多