Decode/separate the Static Challenge format for use with PAM

This forum is for admins who are looking to build or expand their OpenVPN setup.
Forum rules
Please use the [oconf] BB tag for openvpn Configurations. See viewtopic.php?f=30&t=21589 for an example.
Post Reply
adevopsperson
OpenVpn Newbie
Posts: 1
Joined: Wed Jan 26, 2022 4:10 pm

Decode/separate the Static Challenge format for use with PAM

Post by adevopsperson » Wed Jan 26, 2022 4:48 pm

I've inherited an application environment in which users are required to authenticate via Tunnelblick from their Macbooks. There is an Ubuntu box that is dedicated to running OpenVPN Server, version: OpenVPN 2.4.4 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Apr 27 2021.

There is a new requirement to implement MFA on the OpenVPN server. I've been unable to find a single authoritative source of step-by-step instructions on how to perform this implementation. Googling has provided results that either address a use case that is different to mine or suggest partial steps. None of the suggestions I've tried have worked.

In an attempt to implement MFA, I've configured a static challenge in Tunnelblick. From server.conf, I'm calling a custom script prior to invoking the PAM Login module, like so:

script-security 3
auth-user-pass-verify my-custom-script.sh via-env
plugin /usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so login login COMMONNAME password PASSWORD

Each time Tunnelblick contacts the OpenVPN server with my user credentials and my OTP from Google Authenticator, it creates an environment variable called PASSWORD whose value is a base64-encoded concatenation of the string "SRV1", my user password and the OTP using a colon as the delimiter.
SRV1:password_base64:otp_base64

My custom script reads the PASSWORD environment variable, extracts the base64-encoded password, and decodes it into plain text. I think what I need to do is to update the PASSWORD environment variable with the plain text password; but I do not know how to do this, or even if it's possible. I've aware of the existence of the pam_env module and have read its man page, but it's not clear to me how to update the value of an environment variable a value provided by a custom script.
Can anyone identify what I need to do in order to achieve my goal?

User avatar
TinCanTech
Forum Team
Posts: 10823
Joined: Fri Jun 03, 2016 1:17 pm

Re: Decode/separate the Static Challenge format for use with PAM

Post by TinCanTech » Wed Jan 26, 2022 6:18 pm

I have been informed that the information you require can be found here:
https://github.com/OpenVPN/openvpn/blob ... -notes.txt

Edit:

Code: Select all

                  │18:18:17   kitsune1 | wiscii: he is doing it an unnecessarily complicated way. Do not use any script, the OpenVPN's    │
                  │                    | pam plugin is capable of picking apart the base64 password and otp. Change the pam setup to      │
                  │                    | prompt for password and OTP and verify it there.                                                 │
                  │18:20:01   kitsune1 | The plugin line in server config will need to include OTP as well -- like "login USERNAME        │
                  │                    | password PASSWORD pin OTP" -- the exact format depends on his pam set up and prompts used.       │
                  │18:20:28     wiscii | kitsune1: thanks!                                                                                │
There is also information here:
https://community.openvpn.net/openvpn/w ... ionmethods
https://github.com/OpenVPN/openvpn/blob ... E.auth-pam

dorr13
OpenVpn Newbie
Posts: 12
Joined: Fri Jan 07, 2022 12:44 am

Re: Decode/separate the Static Challenge format for use with PAM

Post by dorr13 » Thu Jan 27, 2022 9:16 pm

OpenVPN 2.4.4 openvpn-plugin-auth-pam.so lacks base64 decode functionality. If you upgrade to 2.4.10 or newer, base64 decode is included in openvpn-plugin-auth-pam.so

The encoding only occurs, as far as I know, when static-challenge is used at the client. This is the intuitive way of asking for the second factor after password. If static-challenge is not requested, password will not be base64 encoded.

I don't have a working setup with MFA/2FA which uses static-challenge. I had to drop that and concatenate password and second factor in the password field. This approach will most likely work for version 2.4.4, but your clients may not appreciate having to concatenate. [I am using RADIUS, so slightly different scenario]

dorr13
OpenVpn Newbie
Posts: 12
Joined: Fri Jan 07, 2022 12:44 am

Re: Decode/separate the Static Challenge format for use with PAM

Post by dorr13 » Sat Mar 05, 2022 12:38 am

BTW, openvpn-plugin-auth-pam.so protects the information it is given and operates independently of a script, regardless of order presented in configuration. You can set script-security and receive and decode the password, but the password does not appear to be stored as an accessible environment variable, so having decoded, there is no way to assert the new value. [I tried this too, for the same reason.]

Out of the box, even for openvpn-2.5.1, with current openvpn-plugin-auth-pam.so, the plugin returned properly decoded password only - no TOTP value, decoded or otherwise.

I ended up modifying the plugin so it concatenates password and TOTP after decode on behalf of the client, so it now sends the expected decoded string to PAM.

What I am doing is probably non-standard because of the available RADIUS, so this might not work for anyone else. In the end though, VPN+RADIUS+2FA is working with static-challenge enabled.

User avatar
TinCanTech
Forum Team
Posts: 10823
Joined: Fri Jun 03, 2016 1:17 pm

Re: Decode/separate the Static Challenge format for use with PAM

Post by TinCanTech » Sat Mar 05, 2022 3:24 am

You hacked code and you did not share it .. it will not work for anybody else.

dorr13
OpenVpn Newbie
Posts: 12
Joined: Fri Jan 07, 2022 12:44 am

Re: Decode/separate the Static Challenge format for use with PAM

Post by dorr13 » Wed Mar 16, 2022 11:24 pm

In source openvpn-2.5.6/src/plugins/auth-pam/auth-pam.c, split_scrv1_password function:

Code: Select all

// ---------------------------------------------------------------

static void
split_scrv1_password(struct user_pass *up)
{
    const int skip = strlen("SCRV1:");
    if (strncmp(up->password, "SCRV1:", skip) != 0)
    {
        return;
    }

    char *tmp = strdup(up->password);
    if (!tmp)
    {
        plugin_log(PLOG_ERR, MODULE, "out of memory parsing static challenge password");
        goto out;
    }

    char *pass = tmp + skip;
    char *resp = strchr(pass, ':');
    if (!resp) /* string not in SCRV1:xx:yy format */
    {
        goto out;
    }
    *resp++ = '\0';

    // customization: concatenate pass and resp as pass to meet RADIUS requirements
    pass = strcat(pass, resp);
    // end customization

    int n = plugin_base64_decode(pass, up->password, sizeof(up->password)-1);
    if (n >= 0)
    {
        up->password[n] = '\0';
        n = plugin_base64_decode(resp, up->response, sizeof(up->response)-1);
        if (n >= 0)
        {
            up->response[n] = '\0';
            if (DEBUG(up->verb))
            {
                plugin_log(PLOG_NOTE, MODULE, "BACKGROUND: parsed static challenge password");
            }
            goto out;
        }
    }

    /* decode error: reinstate original value of up->password and return */
    plugin_secure_memzero(up->password, sizeof(up->password));
    plugin_secure_memzero(up->response, sizeof(up->response));
    strcpy(up->password, tmp); /* tmp is guaranteed to fit in up->password */

    plugin_log(PLOG_ERR, MODULE, "base64 decode error while parsing static challenge password");

out:
    if (tmp)
    {
        plugin_secure_memzero(tmp, strlen(tmp));
        free(tmp);
    }
}

// ---------------------------------------------------------------
caveats:
  • Requires re-compile, safest on non-production system [plugin may be copied to production after compile]
  • The modification might be better placed elsewhere
  • This represents a fork which may be contrary to the developer's intent and requires long-term maintenance
  • This is meant to achieve specific behavior [my RADIUS appears to require concatenation of password and response]
  • This is probably the wrong thing to do for everyone (including myself)
  • I would reject this change if I were in charge
My RADIUS vendor indicated Access-Challenge was supported by default, but I don't see that behavior in logs or packet captures, so there is an ongoing dialog about that. If that can be resolved, my modification might go away.

I wouldn't recommend anyone make the modification shown, but if you are in a bind, and you need that behavior, and are comfortable compiling from source, it is an option.
Last edited by TinCanTech on Thu Mar 17, 2022 12:59 am, edited 1 time in total.
Reason: FORMATTING

dorr13
OpenVpn Newbie
Posts: 12
Joined: Fri Jan 07, 2022 12:44 am

Re: Decode/separate the Static Challenge format for use with PAM

Post by dorr13 » Fri Apr 22, 2022 7:05 pm

In case it helps someone:

I also tried a third-party plugin called openvpn-auth-radius ref: https://github.com/brainly/openvpn-auth-radius

On Debian, this is in current packages: apt-get install openvpn-auth-radius
# path to resulting plugin (on current Debian): /usr/lib/openvpn/radiusplugin.so
# path to base config: /usr/share/doc/openvpn-auth-radius/examples/radiusplugin.cnf
# for convenience, radiusplugin.cnf may be copied to the OpenVPN configuration directory and configured
# the plugin reference in OpenVPN server.conf becomes:

server

plugin /usr/lib/openvpn/radiusplugin.so /etc/openvpn/server/radiusplugin.cnf


When tested, authentication might have been faster, but openvpn-auth-radius doesn't appear to handle either static-challenge or dynamic-challenge, so might be viable where no second factor is required. It does not appear to require PAM though, so setup is somewhat simpler. It seems better to use the OpenVPN provided openvpn-plugin-auth-pam.so unless avoiding PAM.

I also tested OpenVPN Management Interface, which reportedly handles dynamic-challenge, but it seems this is designed to be used with custom authentication scripts, which doesn't work for my requirements, so I didn't pursue that. Docs for Management Interface are kind of terse, but the section on dynamic-challenge were practically non-existent.

My requirements have evolved such that TOTP PINs of different lengths must be supported. Under the circumstances, my RADIUS requires separator characters between password and TOTP. So, a new variant of openvpn-plugin-auth-pam.so has been generated to support static-challenge with separator [source: openvpn-2.5.6/src/plugins/auth-pam/auth-pam.c, split_scrv1_password function]:

source

/*
* Split and decode up->password in the form SCRV1:base64_pass:base64_response
* into pass and response and save in up->password and up->response.
* If the password is not in the expected format, input is not changed.
*/
static void
split_scrv1_password(struct user_pass *up)
{
const int skip = strlen("SCRV1:");
if (strncmp(up->password, "SCRV1:", skip) != 0)
{
return;
}

char *tmp = strdup(up->password);
if (!tmp)
{
plugin_log(PLOG_ERR, MODULE, "out of memory parsing static challenge password");
goto out;
}

char *pass = tmp + skip;
char *resp = strchr(pass, ':');
if (!resp) /* string not in SCRV1:xx:yy format */
{
goto out;
}
*resp++ = '\0';

int n = plugin_base64_decode(pass, up->password, sizeof(up->password)-1);
if (n >= 0)
{
up->password[n] = '\0';
n = plugin_base64_decode(resp, up->response, sizeof(up->response)-1);
if (n >= 0)
{
up->response[n] = '\0';
if (DEBUG(up->verb))
{
plugin_log(PLOG_NOTE, MODULE, "BACKGROUND: parsed static challenge password");
}

// modification: concatenate decoded pass, separator, and response, then push result to up
char separator[7];
strcpy(separator,"||||||");
char construct[128];
strcpy(construct,up->password);
strcat(construct,separator);
strcat(construct,up->response);
strcpy(up->password, construct);
// end modification

goto out;
}
}

/* decode error: reinstate original value of up->password and return */
plugin_secure_memzero(up->password, sizeof(up->password));
plugin_secure_memzero(up->response, sizeof(up->response));
strcpy(up->password, tmp); /* tmp is guaranteed to fit in up->password */

plugin_log(PLOG_ERR, MODULE, "base64 decode error while parsing static challenge password");

out:
if (tmp)
{
plugin_secure_memzero(tmp, strlen(tmp));
free(tmp);
}
}


It is still a bad idea to fork code for this, but this code is close to supportable for feature-request. If the option to support concatenation for static-challenge, option to use a separator, and the value of the separator may be read in from an associated config file or passed on the server-config line where the plugin is called, the difficulty of password+TOTP via separate fields might be resolved. [I get that it's easier said than done.]

Alternately, supporting dynamic-challenge via openvpn-plugin-auth-pam.so would resolve the difficulty, but I guess that is a more difficult problem.

User avatar
TinCanTech
Forum Team
Posts: 10823
Joined: Fri Jun 03, 2016 1:17 pm

Re: Decode/separate the Static Challenge format for use with PAM

Post by TinCanTech » Fri Apr 22, 2022 10:17 pm

I recommend that you contact the Openvpn developers.

Post Reply