TLS handshake on EAPOL 802.1X

922 views Asked by At

I am building a EAP-TLS authentication client (802.1X EAPOL). The requirement thus far is just EAP-TLS. I am using the FreeRadius server to test against, and it is using TLS 1.1, so that is the transport version I'm developing to.

Because this supplicant is using a network stack that is custom, and on a small embedded device, I cannot use the OpenSSL libs, since they do all the handshaking as blackbox socket level for communication. Also, the supplicants I found all contain code that is strongly intertwined with the AAA and Authenticator. I don't have that much space to add all that source (in addition to making support more difficult)

It's good to learn while rolling your own anyway.

So, as I dig in, I am seeing things that are not consistent with the RFC's or simply not defined.

Before asking the WPA-Supplicant mailing questions about trying to "roll my own" I first wanted to politely ask simply "Is this a good place to ask technical questions, or is there another resource". I was politely ignored. So I am posting here.

Consulting RFC 3579, 3748, 4346, 5216 and others, I have performed MD5 challenge authentication to the server. Success with understanding EAP, Ethernet packets, fragments, etc.

On to TLS, I have successfully received, assembled and parsed the TLS Server Hello handshake. (RFC 5216 only defines a TLS header over EAP, while RFC 4346 explains the full TLS handshake, but EAP uses a subset of it.) Since I have access to the test server cert and key, I have also verified ciphering a premaster secret with the public key, and it deciphers correctly with the private key.

Now I am trying to build the full Client handshake, piece by piece, adding blocks to the message. And finding things that I cannot resolve.

Below, I am referring to RFC 4346 for the following TLS 1.1 messages.

In Section 4.3, vectors are defined with the specific "Presentation language". Using [] for fixed known lengths, and <..> for variables lengths that must contain a leading value indicating the size.

Section 7.4.7 defines the Client Key Exchange. In my case, it is simply an RSA, therefore is a "EncryptedPreMasterSecret". Section 7.4.7.1 defines the EncryptedPreMasterSecret for RSA, which is the version and random numbers, totaling 48 bytes in length.

enter image description here

The definition does not make any claim about this being a variable vector. And yet, the debug information from FreeRadius rejects it if it does not have a two byte host order value of the length.


(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

Interestingly enough, Wireshark doesn't seem to mind if it's missing.

enter image description here

By adding the two byte length, I am past this failure. However, I don't like that it's not following the specification I read.

Is this described someplace else that I am missing?

So I seem to have gotten past the PremasterSecret, and have moved on to the Certificate Verify message. As for it, section 7.4.8 defines the certificate verify which contains the MD5 and SHA hashes, referring back to section 7.4.3. The definition in 7.4.3 defines what a "Signature" is, and does not make any claim about this being a variable vector.

enter image description here

In fact, section 7.4.3 very clearly indicates that is it a known length vector (i.e. uses fixed lengths [16] and [20]). And yet, Wireshark expects a two byte header here too and reports an error if it is not present.

enter image description here

So I added it the two byte header, Wireshark is happy.

enter image description here

But that is still not following the specification. The known max length is 36 bytes, which fits in one 8 bit number. So requiring two bytes violates the specification which says in section 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.

However even with that change, the server is still complaining.


(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

The server says "decrypt_error". Is this verification message supposed to be encrypted? The spec doesn't say so. Grepping the server source, I cannot find that text message anywhere. It's been hidden very well, making it difficult to find the function that is rejecting it.

And if it is supposed to be encrypted, what key is used? The client private key or the server public key?

Again, is this described someplace else that I am missing? It's not following the specification on two fronts (using a variable length, and two bytes where one is sufficient).

In section 7.4.9 the finished message is defined using the Presentation language containing "[0..11]", which description is not defined anywhere in section 4. Is it a typo meant to be a variable length vector <0..11>? Or what does [0..11] mean here?

Next major question:

Am I making this too hard?

Are there OpenSSL calls which will simply take the reassembled TLS handshake, and create the client handshake reply, populating it into a supplied buffer? Again, because the supplicant client on an embedded device uses it's own network stack, I can't use OpenSSL's internal socket call for the handshake.

The OpenSSL documentation is lacking in many areas, and if such a API exists I have not stumbled across it.

Thanks for any answers and advice.

-Scott

1

There are 1 answers

0
SpacemanScott On

I have this original problem solved, although I don't have answers to all my original questions.

Specifically I will answer the question "Am I making this too hard". That answer is yes.

Turns out the OpenSSL api I am looking for is the BIO api, which I learned is effectively an API that allows you to handle the buffers directly, rather than using the OpenSSL sockets to communicate. Think of this as "man in the middle" by inserting your code in between the sending an receiving calls.

Apparently the BIO api can be used to other things like file IO as well.

In my case this was EAPOL which are raw ethernet packets, but suppose you need to perform some encryption like this TLS handshake over a I2C wire, which has no TCP/IP support. In this case, you would read in the stream into a buffer, copy it into an input BIO, and make the OpenSSL call. Then get the data out of the output BIO and send it.

Here are some specific code fragments which should explain everything:

First, headers. I just threw everything at it, because I am using many of the other features in the code not shown here (OpenSSL pages give this information, but API doc that doesn't tell you where the prototypes are is very annoying to me). Some of the header names are clues as to what they contribute:

#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>

These code fragments below are wrapped in a class, and use the Hungarian notation of a lead "m_" to indicate they are member of the class. First you need a couple of BIO handles, one for what you receive and are passing into OpenSSL, and one for what OpenSSL replies with. There does not appear to be any requirement that you know the buffer size ahead of time.

Declare the BIOS

BIO  *m_sslIn, *m_sslOut;

And initialize them

m_sslIn = NULL;
m_sslOut = NULL;

Now assume SSL is configured, Cert, CAs, Keys, etc... are all loaded and a context has been created (this answer is not meant to be an explanation of SSL calls)

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

Create the BIOs. In and Out are created identically.

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

Now the cool part. Tell OpenSSL that it will use these BIO's as buffers, so you can put data in and take it out.

SSL_set_bio(m_ssl, m_sslIn, m_sslOut);

This is important: I discovered that when you are done, you do NOT need to call BIO_free(). Because the fact that SSL has connected them means it will free them for you. Kind of an annoying feature that's in there without telling anyone. But I was getting "multiple buffer freed" crashes all over the place until I figured that out.

The following assumes that you have data you read off something like the raw packet or I2C wire, or whatever. Note that when you are initialing a TLS handshake, the first message does not yet exist (TLS starts with you sending a ClientHello message to the other side). So leave the input BIO empty on the first call and make a call SSL_connect(m_ssl) to get started.

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");
    }
}

The SSL_connect() call WILL give a failure. But the failure is the error: SSL_ERROR_WANT_READ, which simply means that it is waiting for more to reads. So, right after the call, copy out what OpenSSL put in the output 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

On the first pass, you will have the beginning "CLIENT_HELLO" message.

So, now you have the message in your buffer, send it to the server, and read the reply from the server. Take that reply and store it in the input BIO again, call SSL_connect() again, and read the response out of the BIO. Just keep going back and forth until the handshake completes.

Also, as shown in the code fragment above, each time you call SSL_connect, you will have to flush out all the errors that are piled in the SSL handle:

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;
}

For my EAPOL client, this process turned out to be quite simple. The complex part was handling the fragmentation of the messages that were going back and forth, because some TLS handshakes can have many certificates and be quite large. And it is important that the entire message from the server be reassembled before putting it into the BIO and calling the next SSL_connect.

I hope this helps the next person trying to so the same thing.