4

I am trying to implement a simple SMTP server using Vala and GLib + GIO.

Plain text communication is no problem so far, but when it comes to TLS using STARTTLS things get harder.

This is the code I have so far:

const string appname = "vsmtpd";
const string hostname = "myserver";
const uint16 listenport = 10025;
const string keyfile = "vsmtpd.key";
const string certfile = "vsmtpd.crt";
// TODO: Parse EHLO instead of constant string
const string username = "myclient";

void process_request_plain (InputStream input, OutputStream output) throws Error {
    output.write (@"220 $hostname ESMTP $appname\n".data);
    var data_in = new DataInputStream (input);
    string line;
    while ((line = data_in.read_line (null)) != null) {
        stdout.printf ("%s\n", line);
        line = line.chomp ();
        if (line.substring (0, 5) == "EHLO ") {
            output.write (@"250-$hostname Hello $username\n".data);
            output.write ("250 STARTTLS\n".data);
        }
        else if (line == "STARTTLS") {
            output.write ("220 Go ahead\n".data);
            break;
        }
        else {
            output.write ("502 Command not implemented\n".data);
        }
    }
}

int main () {
    try {
        TlsCertificate cert = new TlsCertificate.from_files
            (certfile, keyfile);
        var service = new SocketService ();
        service.add_inet_port (listenport, null);
        service.start ();
        while (true) {
            SocketConnection conn = service.accept (null);
            process_request_plain (conn.input_stream, conn.output_stream);
            TlsServerConnection tlsconn = TlsServerConnection.@new (conn, cert);
            assert_nonnull (tlsconn);
            // TODO: Is this neccessary?
            tlsconn.accept_certificate.connect ((peer_cert, errors) => {
                stdout.printf ("TLS accepting peer cert\n");
                return true;
            });
            try { 
                tlsconn.handshake ();
                stdout.printf ("TLS handshake ok\n");
            } catch (Error e) {
                stdout.printf ("TLS handshake failed\n");
                stderr.printf ("%s\n", e.message);
            }
        }
    } catch (Error e) {
        stderr.printf ("%s\n", e.message);
    }
    return 0;
}

Given a valid SSL certificate in vsmtpd.key and vsmtpd.crt (which I generated with openssl req -x509 -newkey rsa:2048 -keyout vsmtpd.key -out vsmtpd.pem -days 365 -nodes) I start the program and I also run this OpenSSL command to test STARTTLS:

openssl s_client -connect localhost:10025 -starttls smtp -debug

The output from my program is:

EHLO openssl.client.net
STARTTLS
TLS handshake failed
Stream is already closed

The output from OpenSSL is:

CONNECTED(00000003)
read from 0x6ae470 [0x6af050] (4096 bytes => 26 (0x1A))
0000 - 32 32 30 20 6d 79 73 65-72 76 65 72 20 45 53 4d   220 myserver ESM
0010 - 54 50 20 76 73 6d 74 70-64 0a                     TP vsmtpd.
write to 0x6ae470 [0x6b0060] (25 bytes => 25 (0x19))
0000 - 45 48 4c 4f 20 6f 70 65-6e 73 73 6c 2e 63 6c 69   EHLO openssl.cli
0010 - 65 6e 74 2e 6e 65 74 0d-0a                        ent.net..
read from 0x6ae470 [0x6af050] (4096 bytes => 28 (0x1C))
0000 - 32 35 30 2d 6d 79 73 65-72 76 65 72 20 48 65 6c   250-myserver Hel
0010 - 6c 6f 20 6d 79 63 6c 69-65 6e 74 0a               lo myclient.
read from 0x6ae470 [0x6af050] (4096 bytes => 13 (0xD))
0000 - 32 35 30 20 53 54 41 52-54 54 4c 53 0a            250 STARTTLS.
write to 0x6ae470 [0x7ffdb4aea9e0] (10 bytes => 10 (0xA))
0000 - 53 54 41 52 54 54 4c 53-0d 0a                     STARTTLS..
read from 0x6ae470 [0x6a13a0] (8192 bytes => 13 (0xD))
0000 - 32 32 30 20 47 6f 20 61-68 65 61 64 0a            220 Go ahead.
write to 0x6ae470 [0x6aefa0] (204 bytes => 204 (0xCC))
0000 - 16 03 01 00 c7 01 00 00-c3 03 03 0e ac 05 35 45   ..............5E
0010 - db 95 f6 a7 37 55 d8 ca-14 d7 5f 8e 6a 62 08 50   ....7U...._.jb.P
0020 - c9 81 b7 55 75 a8 4c 17-c0 a1 53 00 00 76 00 a5   ...Uu.L...S..v..
0030 - 00 a3 00 a1 00 9f 00 6b-00 6a 00 69 00 68 00 39   .......k.j.i.h.9
0040 - 00 38 00 37 00 36 00 88-00 87 00 86 00 85 00 9d   .8.7.6..........
0050 - 00 3d 00 35 00 84 00 a4-00 a2 00 a0 00 9e 00 67   .=.5...........g
0060 - 00 40 00 3f 00 3e 00 33-00 32 00 31 00 30 00 9a   .@.?.>.3.2.1.0..
0070 - 00 99 00 98 00 97 00 45-00 44 00 43 00 42 00 9c   .......E.D.C.B..
0080 - 00 3c 00 2f 00 96 00 41-00 07 00 05 00 04 00 16   .<./...A........
0090 - 00 13 00 10 00 0d 00 0a-00 15 00 12 00 0f 00 0c   ................
00a0 - 00 09 00 ff 02 01 00 00-23 00 23 00 00 00 0d 00   ........#.#.....
00b0 - 16 00 14 06 01 06 02 05-01 05 02 04 01 04 02 03   ................
00c0 - 01 03 02 02 01 02 02 00-0f 00 01 01               ............
read from 0x6ae470 [0x6b4500] (7 bytes => -1 (0xFFFFFFFFFFFFFFFF))
write:errno=104
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 80 bytes and written 239 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
---

What I understand from the output my program closes the connection before the TLS handshake can complete. (I also tried using Thunderbird and Claws Mail)

What am I doing wrong here?

PS: I couldn't find any example on how to use GTLsServerConnection in a STARTTLS situation.

Update:

I tried -ssl2, -ssl3, -tls1, -tls1_1, -tls1_2 options of OpenSSL which also don't work.

openssl s_client -connect localhost:10025 -starttls smtp -state

yields:

CONNECTED(00000003)
SSL_connect:before/connect initialization
SSL_connect:SSLv2/v3 write client hello A
SSL_connect:error in SSLv2/v3 read server hello A
write:errno=104
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 100 bytes and written 239 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
---

So the client sends "client hello A", but the server doesn't send a correct "server hello A".

As an alternative you can also try gnutls-cli --crlf --starttls-proto=smtp --port 10025 localhost.

The output from GNUTLS_DEBUG_LEVEL=11 ./vsmtpd is:

gnutls[2]: Enabled GnuTLS logging...
gnutls[2]: Intel SSSE3 was detected
gnutls[2]: Intel AES accelerator was detected
gnutls[2]: Intel GCM accelerator was detected
gnutls[2]: Enabled GnuTLS logging...
gnutls[2]: Intel SSSE3 was detected
gnutls[2]: Intel AES accelerator was detected
gnutls[2]: Intel GCM accelerator was detected
gnutls[3]: ASSERT: x509_b64.c:299
gnutls[9]: Could not find '-----BEGIN RSA PRIVATE KEY'
gnutls[3]: ASSERT: x509_b64.c:299
gnutls[9]: Could not find '-----BEGIN DSA PRIVATE KEY'
gnutls[3]: ASSERT: x509_b64.c:299
gnutls[9]: Could not find '-----BEGIN EC PRIVATE KEY'
gnutls[3]: ASSERT: privkey.c:503
gnutls[2]: Falling back to PKCS #8 key decoding
EHLO openssl.client.net
STARTTLS
gnutls[5]: REC[0xfa67e0]: Allocating epoch #0
gnutls[3]: ASSERT: gnutls_constate.c:586
gnutls[5]: REC[0xfa67e0]: Allocating epoch #1
gnutls[3]: ASSERT: gnutls_buffers.c:1138
gnutls[10]: READ: -1 returned from 0xfa4120, errno=0 gerrno=5
gnutls[3]: ASSERT: gnutls_buffers.c:364
gnutls[3]: ASSERT: gnutls_buffers.c:572
gnutls[3]: ASSERT: gnutls_record.c:1058
gnutls[3]: ASSERT: gnutls_record.c:1179
gnutls[3]: ASSERT: gnutls_buffers.c:1392
gnutls[3]: ASSERT: gnutls_handshake.c:1428
gnutls[3]: ASSERT: gnutls_handshake.c:3098
gnutls[3]: ASSERT: gnutls_db.c:334
TLS handshake failed
Stream is already closed
gnutls[5]: REC[0xfa67e0]: Start of epoch cleanup
gnutls[5]: REC[0xfa67e0]: End of epoch cleanup
gnutls[5]: REC[0xfa67e0]: Epoch #0 freed
gnutls[5]: REC[0xfa67e0]: Epoch #1 freed
Promi
  • 318
  • 3
  • 11
  • 1
    This would serve as a good example if you can get it working. You are atleast getting a response - (7 bytes => -1 (0xFFFFFFFFFFFFFFFF)). You could try a few variations of openssl options as suggested in http://stackoverflow.com/questions/24457408/openssl-command-to-check-if-a-server-is-presenting-a-certificate – AlThomas Oct 06 '15 at 13:16
  • I'm not sure there is a problem with the Vala code. https://developer.gnome.org/gio/stable/GTlsConnection.html#g-tls-connection-handshake says handshake can be called "if you want to know for sure whether the initial handshake succeeded or failed". So the "Stream already closed" is as expected after an initial handshake failure. The problem could be your certificate. I note you use "myserver" but connect to "localhost". Check the server name? – AlThomas Oct 07 '15 at 09:59
  • 1
    Also I believe GnuTLS is the backend so try setting GNUTLS_DEBUG_LEVEL - see http://gnutls.org/manual/html_node/Debugging-and-auditing.html You could also try ssldump to listen in on the connetion, e.g. https://www.sslshopper.com/article-debugging-ssl-communications.html – AlThomas Oct 07 '15 at 10:05
  • @AIThomas: I've added the debug output from gnutls and also added the command i used to create the key files. – Promi Oct 07 '15 at 17:13
  • OK, that's informative. It looks as though it might not be finding the private key. Probably needs to be in /etc/ssl/certs/ Also I'm getting the impression you don't understand about domain names and certificates. You also need to understand about Certificate Authorities and self-signed certificates. This may give some pointers: https://help.ubuntu.com/community/GnuTLS – AlThomas Oct 07 '15 at 17:59
  • I just tried to go from STARTTLS to SMTPS (no plain text at the beginning, direct handshake after connection). That works! -ssl3, -tls1, -tls1_1 and -tls1_2 work fine that way. – Promi Oct 07 '15 at 18:05
  • That's not what I'm after though, but it means that the certificate loading is fine. Upgrading from plain text to TLS after STARTTLS is not working. – Promi Oct 07 '15 at 18:06
  • Good you got it working – AlThomas Oct 07 '15 at 18:54

1 Answers1

2

The problem is hidden somewhere in the implementation of DataInputStream.

Once I removed it and used the following replacement for read_line () instead, it works just fine.

string? read_line (InputStream input) throws Error {
    var buffer = new uint8[1];
    var sb = new StringBuilder ();
    buffer[0] = '\0';
    while (buffer[0] != '\n') {
        input.read (buffer);
        sb.append_c ((char) buffer[0]);
    }
    return (string) sb.data;
}

void process_request_plain (InputStream input, OutputStream output) throws Error {
    output.write (@"220 $hostname ESMTP $appname\n".data);
    string line;
    while ((line = read_line (input)) != null) {
        stdout.printf ("%s\n", line);
        line = line.chomp ();
        if (line.substring (0, 5) == "EHLO ") {
            output.write (@"250-$hostname Hello $username\n".data);
            output.write ("250 STARTTLS\n".data);
        }
        else if (line == "STARTTLS") {
            output.write ("220 Go ahead\n".data);
            break;
        }
        else {
            output.write ("502 Command not implemented\n".data);
        }
    }
}
Promi
  • 318
  • 3
  • 11