2

Full title should be "PHP - Sending Emails: fwrite(): SSL operation failed with code 1. OpenSSL Error messages:error:1420C0CF:SSL routines:ssl_write_internal:protocol is shutdown - analyze stream_socket_enable_crypto returning false". But this was too long for the title's 150 characters.

We are trying to send an email in a PHP script and we're getting

Warning: fwrite(): SSL operation failed with code 1. OpenSSL Error messages:error:1420C0CF:SSL routines:ssl_write_internal:protocol is shutdown in <...>/vendor/recolize/zendframework1/library/Zend/Mail/Protocol/Abstract.php on line 324 

As seen above we're using an adaption version of the long outdated zendframework1, but that is not the issue at hand as this works on two other hosters.

As the issue is non-deterministic we have no way to reproduce it right now.

However, we have a test script that allows some analysis, but I need help understanding what might be the problem.

Without further ado:

We're using the script from this stackoverflow answer: How do I verify a TLS SMTP certificate is valid in PHP? - Answer 1

The important part is this:

// Establish the connection
$smtp = fsockopen( "tcp://$server", 25, $errno, $errstr );
fread( $smtp, 512 );

// Here you can check the usual banner from $server (or in general,
// check whether it contains $server's domain name, or whether the
// domain it advertises has $server among its MX's.
// But yet again, Google fails both these tests.

fwrite($smtp,"HELO $myself\r\n");
fread($smtp, 512);

// Switch to TLS
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, true);
stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
stream_context_set_option($smtp, 'ssl', 'allow_self_signed', true);
stream_context_set_option($smtp, 'ssl', 'capture_peer_cert', true);
stream_context_set_option($smtp, 'ssl', 'cafile', $cabundle);

// Necessary block to circumvent https://www.php.net/manual/en/function.stream-socket-enable-crypto.php#119122
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
    $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
    $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}

$secure = stream_socket_enable_crypto($smtp, true, $crypto_method);

// $secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT );
var_dump($secure);
die;

A socket is established, set to blocking, SSL options are set and a try is made to start encryption using PHP's stream_socket_enable_crypto.

stream_socket_enable_crypto however returns bool(false) and no information why it fails.

Currently we are suspecting that the verification of the mail server's certificate fails, but we can't analyse it.

How can we get more information why stream_socket_enable_crypto returns false?

edit1:

Adapted the code block above to reflect STREAM_CRYPTO_METHOD_TLS_CLIENT's specialty since PHP 5.6.7 as mentioned by @Dilek below. Thank you for the answer! Sadly this did not change the overall result. $secure stays bool(false).

edit2:

In response to @Dilek's UPDATE:

$ php ssl_test.php
Array
(
    [ssl] => Array
        (
            [verify_host] => 1
            [verify_peer] => 1
            [allow_self_signed] =>
            [cafile] => /etc/ssl/cert.pem
        )

)
failed to connect securely

I made sure the cafile exists:

$ ll /etc/ssl/cert.pem
0 lrwxrwxrwx  1 root  wheel  38 Dec 19 11:09 /etc/ssl/cert.pem -> /usr/local/share/certs/ca-root.crt

and quick-checked its contents which seems fine:

Certificate:
    Data:
        Version: ...
        Serial Number:
            ...
        Signature Algorithm: ...
        Issuer: ...
        ...
    Signature Algorithm: ...
-----BEGIN CERTIFICATE-----
MI...6EULg==
-----END CERTIFICATE-----

Certificate:
    Data:
        Version: ...
        Serial Number:
            ...

etc.
Worp
  • 948
  • 4
  • 17
  • 30
  • Just out of curiosity: sending mails the plain way is already pretty difficult (that's why there are multiple libraries out in the wild to keep you out of trouble). Now, you are adding another layer of difficulty by talking plain SMTP first - what's the reason for this? – Nico Haase Jan 23 '20 at 11:08
  • I'm trying to get to the bottom of the issue or at least find some hint about errors occuring. I found the script in the stackoverflow link provided above and currently it's my best idea how to maybe get additional information. So i'm really just trying to talk plain SMTP because of a lack of alternative ideas. The original email problem producing the error above (warning: fwrite()...) comes from a framework (zendframework1), but we can't reproduce it deterministically. The current script is really our only idea right now. I'm happy to try any alternatives to get to the bottom of this. – Worp Jan 23 '20 at 11:13
  • To provide alternative ideas, one needs to understand the need of that script. What do you want to achieve? Sending mails is way easier using PHPMailer, Swiftmailer, even with the plain `mail` function. – Nico Haase Jan 23 '20 at 12:35
  • I will produce a script using PHPMailer and update my answer. – Worp Jan 23 '20 at 12:54
  • We found the answer after all. I posted it. – Worp Jan 23 '20 at 14:01

2 Answers2

1

PHP bug : https://bugs.php.net/bug.php?id=69195

Commit : https://github.com/php/php-src/commit/10bc5fd4c4c8e1dd57bd911b086e9872a56300a0

STREAM_CRYPTO_METHOD_SSLv23_CLIENT is not safe to use because before php 5.6.7, it means sslv2 or sslv3. So, you should do this:

$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;

if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
    $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
    $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}

stream_socket_enable_crypto($socket, true, $crypto_method);

You should read bottom of the page you linked in question.

Documentation : https://www.php.net/manual/en/function.stream-socket-enable-crypto.php

UPDATE : your code should look like this

            $smtp = fsockopen( "tcp://mail.domain.com", 25, $errno, $errstr );
            fread( $smtp, 512 );
            fwrite($smtp,"HELO worp\r\n");
            fread($smtp, 512);
            fwrite($smtp,"STARTTLS\r\n");
            fread($smtp, 512);
            stream_set_blocking($smtp, true);
            stream_context_set_option($smtp, 'ssl', 'verify_host', true);
            stream_context_set_option($smtp, 'ssl', 'verify_peer', true);
            stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);

//Self signed certificates are blocked by googl and most servers

            stream_context_set_option($smtp, 'ssl', 'cafile', '/etc/ssl/cacert.pem');
    //you need to add correct and full path of CA file 

    //This is second solution in php documents in the link in question and in my answer
            $secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
            stream_set_blocking($smtp, false);
            print_r(stream_context_get_options($smtp));
            if( ! $secure)
                    die("failed to connect securely\n");
            print "Success!\n";

Hope this help you.

  • I have edited the code above, good mention. I had read that and had manually tried around with various constants but I had not actually used the entire block. I did now try it with the full block from your php.net link but stream_socket_enable_crypto continues to return false. – Worp Jan 23 '20 at 09:41
  • 1
    *stream_socket_enable_crypto continues to return false* After several investigations, it turns out that PHP on my local machine was not setup properly to validate trusted CAs. Please see Here : https://stackoverflow.com/questions/53437771/reasons-why-php-stream-socket-enable-crypto-returns-false I would give that as an answer but some would duplicate my answer with all their respect :) –  Jan 23 '20 at 09:51
  • I would assume you are correct and that the answer is correct but I don't know in what way it is set up wrongly. I had hoped that stream_socket_enable_crypto would give me a hint why it can't establish the encryption to get a pointer what the false configuration might be. We have the cert chain set up, we have allowed self-signed (see above in the code), I have tried with and without verify_peer. But nothing would yield information what the actual problem with the certificate or establishing SSL in general is. That'd be the hint I'm looking for. – Worp Jan 23 '20 at 10:00
  • 1
    @Worp would you please try this one instead of the code in my answer : `stream_set_blocking ($smtp, true); stream_socket_enable_crypto ($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);stream_set_blocking ($smtp, false);` And make sure if your **CA path is correct** see detail here :https://curl.haxx.se/docs/caextract.html AND please see this answer for **complete answer** https://stackoverflow.com/a/13445297/12232340 let me know. –  Jan 23 '20 at 10:15
  • Updated my answer with your UPDATE (see "edit2" above). I also provided some assurance about the cafile's location and contents. However at this point I'm wondering: Might the contents of the cafile be incorrect? (This is provided by a hoster) – Worp Jan 23 '20 at 11:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/206513/discussion-between-worp-and-dilek). – Worp Jan 23 '20 at 11:13
  • We found the answer. Something very basic was wrong, although not the cafile. I posted the answer. Thank you for your time! I really appreciate it greatly. – Worp Jan 23 '20 at 13:59
  • Just changing helo to ehlo made it works ? :) the problem was in here `fwrite($smtp,"HELO worp\r\n");` I think, I put your nickName into url and tought you will change it to your url and forgotten about it :) `fwrite($smtp,"HELO worp\r\n");` see `worp` in there should be your domain url, damn me :) Glad you solved –  Jan 23 '20 at 14:16
  • I think you could even leave worp in there. It's literally EHLO instead of HELO ^^ Thanks again for your help! – Worp Jan 23 '20 at 18:36
0

The final answer is: Use EHLO statt HELO when talking SMTP.

Reason: HELO does not support STARTTLS.

The final script:

$smtp = fsockopen( "tcp://mail.noris.net", 25, $errno, $errstr );
fread( $smtp, 512 );
fwrite($smtp,"EHLO test.de\r\n");
fread($smtp, 512);
fwrite($smtp,"STARTTLS\r\n");
fread($smtp, 512);
stream_set_blocking($smtp, true);
stream_context_set_option($smtp, 'ssl', 'verify_host', false);
stream_context_set_option($smtp, 'ssl', 'verify_peer', false);
stream_context_set_option($smtp, 'ssl', 'allow_self_signed', false);

//Self signed certificates are blocked by googl and most servers

stream_context_set_option($smtp, 'ssl', 'cafile', '/etc/ssl/cert.pem');
//you need to add correct and full path of CA file

//This is second solution in php documents in the link in question and in my answer
$secure = stream_socket_enable_crypto($smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
stream_set_blocking($smtp, false);
print_r(stream_context_get_options($smtp));
if( ! $secure) {
    print("failed to connect securely\n");
}
else {
    print "Success!\n";
}

var_dump($errno);
var_dump($errstr);
die;

Output:

$ php ssl_test.php
Array
(
    [ssl] => Array
        (
            [verify_host] =>
            [verify_peer] =>
            [allow_self_signed] =>
            [cafile] => /etc/ssl/cert.pem
        )

)
Success!
int(0)
string(0) ""
Worp
  • 948
  • 4
  • 17
  • 30