Hi again,

I have updated the nonce generation subroutine according to the recommendations in RFC 5116, chapter 3.2, "Recommended Nonce Formation". I have added a nonce counter to the implementation, while keeping the old stuff. This SHOULD be sufficient to keep the nonces unique:

char *get_random_string (void)
{
        static bool _first_run = true;
        struct timespec tv, tv2;
        static struct drand48_data buffer;
        double result, result2;
        char *randomstr = NULL;
        char *nonce_ctr = get_nonce_ctr();
        char * retval = NULL;

        if (nonce_ctr == NULL)
                goto get_random_exit2;

        if (clock_gettime (CLOCK_MONOTONIC, &tv) == -1)
                goto get_random_exit;

        if (clock_gettime (CLOCK_PROCESS_CPUTIME_ID, &tv2) == -1)
                goto get_random_exit;

        if (_first_run) {
                _first_run = false;
                srand48_r (tv2.tv_nsec, &buffer);
        }
        drand48_r (&buffer, &result);
        drand48_r (&buffer, &result2);

        int ret = asprintf (&randomstr, "%lf:%ld:%ld:%ld:%ld:%d:%lf:%s",
                                result, tv.tv_sec, tv.tv_nsec,
                                tv2.tv_sec, tv2.tv_nsec, getpid(), result2, nonce_ctr);

        if (ret != -1 && randomstr != NULL)
                retval = sha256_string (randomstr);

        if (get_serial_debug)
                fprintf (stderr, "randomstr = %s\n", randomstr);

        if (randomstr != NULL)
                free (randomstr);
get_random_exit:
        if (nonce_ctr != NULL)
                free (nonce_ctr);
get_random_exit2:
        return retval;
}

I have also added authorization request sequence serial numbers (ARSSNs). These would like DNS zone serials. The request is protected from tampering with an RFC 2104-style HMAC using:

H(str) ::= sha256sum(str)
N = H(rnd)
HMAC = H ( N || username || password || clientIP || serial || secret || N )
HMACret = H ( N || serial || secret || N )

Nonce is re-added at the end of "data" to prevent length extension attacks in theory.

Authorization in the script goes like this:

<?php

$serial_file = "/usr/local/etc/myauth/serial";

$ip_address = $_SERVER['REMOTE_ADDR'];
$ip_srv_address = $_SERVER['SERVER_ADDR'];

if ( $ip_address !== $ip_srv_address )
{
        header("HTTP/1.1 403 Forbidden");
        echo "HOST NOT PERMITTED";
        exit(0);
}
else if( isset($_POST["user"]) && isset($_POST["pass"]) && isset($_POST["mode"]) )
{
        $ret=-1;

        $nonce = $_POST["nonce"];
        $serial = $_POST["serial"];
        $sha256 = $_POST["hash"];
        if (($rawsecret = file_get_contents("/usr/local/etc/myauth/secret")) !== false) {
                $secret = trim($rawsecret);
                $mysha256 = hash("sha256", $nonce . $_POST["user"] . $_POST["pass"] . $_POST["mode"] . $_POST["clientIP"] . $_POST["serial"] . $secret . $nonce);
                if ($sha256 !== $mysha256) {
                        $secret = "";
                        $ret = 401;
                } else {
                        $rethash = hash("sha256", $nonce . $serial . $secret . $nonce);
                        $secret = "";
                        $ret = 0;
                }
        } else {
                $ret = 402;
                $secret = "";
        }

        if ( $ret !== 0 ) {
                header("HTTP/1.1 $ret Forbidden");
                echo "HOST NOT PERMITTED";
                exit(0);
        }

        if ( $ret == 0 && ($rawserial = file_get_contents($serial_file)) !== false) {
                $myserial = trim($rawserial);
                if ($myserial >= $serial)
                        $ret = 405;
                else {
                        // remote serial is greater, we are going to the next stage                         if (file_put_contents($serial_file, $serial) == false)
                                $ret = 406;
                        else
                                $ret = 0;
                }
        } else
                $ret = 404;

        if ( $ret !== 0 ) {
                header("HTTP/1.1 $ret Forbidden");
                echo "HOST NOT PERMITTED";
                exit(0);
        }

        switch($_POST["mode"])
        {

        // additional username checking ...

So, if authorization request serial counter unlikely flips over INT64_MAX, it is rolls over to 1, and future
authorizations are impossible until the administrator intervention.

This works in theory, however the implementation may still be buggy. I am hoping to get this working so I could perform additional PAM authorizations like temporary locking out of certs without revoking
them completely, working days and working hours access etc. ...

I am planning to disseminate the code on GitHub, and make the installation more user-friendly, however the code may need to be more stable before publishing if you know what I mean. I started HMAC authentication on Saturday afternoon, and several times I thought I had it, but
the circumstances proved me otherwise.

I attribute my progress to the highly motivating atmosphere on the project:-)  and the help from Above.

Any idea? Am I on the right track?

(Of course, this is still just as secure as the shell account's password and the www-data user
running PHP scripts.)

RATIONALE is:

Since the original pam_url did not authenticate either pam_url module host and neither the authentication PHP script running module, it seemed necessary to authenticate them and using HMAC-SHA-256 seemed as a logical solution, when compared to transmission of secret in cleartext
over TLS channel, which later also implied the use of nonces and serials
that prevent replay attack in case that the TLS session had theoretically been recorded for offline
brute force or other form of man-in-the-middle attack.

Kind regards,
Mirsad

On 7.2.2022. 8:58, Mirsad Goran Todorovac wrote:
Hi Paul,

On 7.2.2022. 1:56, Paul Wouters wrote:
On Sun, 6 Feb 2022, Mirsad Goran Todorovac wrote:

The passwordless authentication over pam_url used with IKEv2 with the certificates was considered a source of brute force attacks and a dangerous module to implement for it could allow everyone to access the system if accidentally left as the only and sufficient module in PAM stack.

You can't really brute force the certificate validation part.

The pam module is just an _additional_ restriction that can restrict an
otherwise validated certificate. It is never even called for invalid,
bad or revoked certificates as the connection is rejected before the pam
phase due to the failed verification.
Yes. Agreed. But: if TLS is somehow compromised (i.e. by a quantum computer attack), I would still want to be able to authenticate safely. Or as safe as my hash function (currently being SHA-256, but nothing
prevents using a stronger one ...).

I try this by sending message digest:

H(str) = sha256sum(str);
N = H(rnd);
HMAC = H( N || username || password || clientIP || secret || N);
retHMAC = H( N || secret || N);

So, the authorization script MUST prove knowledge of the secret and my nonce N as a challenge
by sending retHMAC which I check.

It can only do so if knowing the nonce of the session N and the pre-shared secret.

The key is to generate unique nonce. I do so like this:

char *get_random_string (void)
{
        static bool _first_run = true;
        struct timespec tv, tv2;
        static struct drand48_data buffer;
        double result, result2;
        char *randomstr = NULL;

        if (clock_gettime (CLOCK_MONOTONIC, &tv) == -1)
                return NULL;

        if (clock_gettime (CLOCK_PROCESS_CPUTIME_ID, &tv2) == -1)
                return NULL;

        if (_first_run) {
                _first_run = false;
                srand48_r (tv2.tv_nsec, &buffer);
        }
        drand48_r (&buffer, &result);
        drand48_r (&buffer, &result2);
        int ret = asprintf (&randomstr, "%lf%ld%ld%ld%ld%d%lf",
                                result, tv.tv_sec, tv.tv_nsec,
                                tv2.tv_sec, tv2.tv_nsec, getpid(), result2);
        if (ret == -1)
                return NULL;
        char * hashstr = sha256_string (randomstr);
        free (randomstr);
        return hashstr;
}

So, I am not sure that I get enough randomness i.e. if the PAM module is (theoretically) called for the second time within the same nanosecond (or time resolution interval of the clock, which can be of lesser
quality!).

So, the main question appears to be if there is a smarter way of preventing brute force replay attacks

IKE has build-in protection against replay attacks. Both sides you a
nonce for different connection attempts. So it is always different and
there is no replaying possible.
Indeed, but I do not authenticate the second stage pam_url over IKE, but over TLS connection instead to a CGI-bin module. In particular, this module is vulnerable to brute force attacks. I defend my system by limiting to IP address, but that is easily defeated. So I thought of knowing a common secret like RADIUS
protocol.

In particular, I did not want to send the secret in cleartext even over TLS connection, as i.e. private key might have been compromised or brute force cracked by a quantum computer by yet undocumented
man-in-the-middle attack.

I defend against these like this:

$ip_address = $_SERVER['REMOTE_ADDR'];
$ip_srv_address = $_SERVER['SERVER_ADDR'];

if ( $ip_address !== $ip_srv_address )
{
        header("HTTP/1.1 403 Forbidden");
        echo "HOST NOT PERMITTED";
        exit(0);
}
else if( isset($_POST["user"]) && isset($_POST["pass"]) && isset($_POST["mode"]) )
{
        $ret=-1;

        $nonce = $_POST["nonce"];
        $sha256 = $_POST["hash"];
        if (($rawsecret = file_get_contents("/usr/local/etc/myauth/secret")) !== false) {
                $secret = trim($rawsecret);
                $mysha256 = hash("sha256", $nonce . $_POST["user"] . $_POST["pass"] . \                                            $_POST["mode"] . $_POST["clientIP"] . $secret . $nonce);
                if ($sha256 !== $mysha256) {
                        $secret = "";
                        $ret = 401;
                } else {
                        $rethash = hash("sha256", $nonce . $secret . $nonce);
                        $secret = "";
                        $ret = 0;
                }
        } else {
                $ret = 402;
        }

        if ( $ret == 0 )
        switch($_POST["mode"])
        {
                case "PAM_SM_AUTH";
                        // Perform authing here
                case "PAM_SM_ACCOUNT";
                        // Perform account aging here

// .... the authorization code for the certificate ....

So, I wonder how to generate unique-enough nonce, so my authorization process is safe.
(Safe as SHA256(str) hash at most.)

In case of broken or compromised TLS, nothing prevents the adversary to record nonce and SHA256 hash and reuse them. I defend against this by using rethash, which is a proof that the authorization service is in possession of the nonce for this session (which must not be reused), and in possession of the secret key (which we hope the adversary had not learned, for it is
never transmitted in clear like in PAP.

I thought of maintaining some serial or sequence number for the authorization process which would also be protected by the SHA256 hash, incremented and never repeated, but I still
haven't thought of a good implementation of this feature.

I hope this explains.

I know about sequence numbers in IKE as the protection from the replay attacks (I don't know this in detail though), but let me remind you that pam_url doesn't authenticate over IKE nor
IPsec, so if we do not trust TLS, completely we open a host of issues ...

Kind regards,
Mirsad

--
Mirsad Todorovac
CARNet system engineer
Faculty of Graphic Arts | Academy of Fine Arts
University of Zagreb
Republic of Croatia, the European Union
--
CARNet sistem inženjer
Grafički fakultet | Akademija likovnih umjetnosti
Sveučilište u Zagrebu
_______________________________________________
Swan mailing list
[email protected]
https://lists.libreswan.org/mailman/listinfo/swan

Reply via email to