diff --git a/NEWS b/NEWS index 405104ad..78fdc05c 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,8 @@ affected session to be rejected rather than silently ignored. A configured default-user-conf or default-group-conf file that cannot be opened also now causes session rejection (previously silently ignored). +- In certificate authentication the user is not prompted for group selection + if the certificate provides a single group (#692) * Version 1.5.0 (released 2026-06-07) diff --git a/doc/ocserv.8.md b/doc/ocserv.8.md index 3c7b04a3..60faae1f 100644 --- a/doc/ocserv.8.md +++ b/doc/ocserv.8.md @@ -189,9 +189,18 @@ possession of the corresponding private key. The certificate need also contain user identifying information, for example, the user ID of the client must be embedded in the certificate's Distinguished Name (DN), i.e., in the Common Name, or UID fields. For the -server to read the name, the *cert-user-oid* configuration option +server to read the user ID, the *cert-user-oid* configuration option must be set. +When *cert-group-oid* is configured the server extracts group names from the +client certificate's DN and presents them for selection. If the client does +not request a group and exactly one eligible group is present in the +certificate — where eligible means either no *select-group* list is configured, +or the group appears in the configured *select-group* list — the server +automatically selects it and completes authentication without an extra round +trip. If zero or more than one eligible groups are found the client is +prompted to choose. + The following examples demonstrate how to use certtool from GnuTLS to generate such CA. diff --git a/doc/requirements/internal/authentication.md b/doc/requirements/internal/authentication.md index b01168ee..92e50fee 100644 --- a/doc/requirements/internal/authentication.md +++ b/doc/requirements/internal/authentication.md @@ -407,6 +407,36 @@ REQ-AUTH-AUTH-005. **Links:** REQ-AUTH-INIT-002, REQ-AUTH-INIT-005, REQ-AUTH-AUTH-005, REQ-AUTH-AUTH-006, REQ-AUTH-AUTH-008 +### REQ-AUTH-AUTH-041 — Worker auto-selects the certificate group when exactly one is eligible, avoiding an unnecessary group-selection prompt + +**Requirement:** In `post_auth_handler()`, when `AUTH_TYPE_CERTIFICATE` is +active, `ws->cert_groups_size > 0`, and the client did not request a group +(no `group_list`/`group-select` field, no `select-group-by-url` match, and +`default-select-group` not requested), the worker MUST NOT unconditionally +respond with the "Please select your group." prompt. It MUST first compute +the set of *eligible* certificate groups: if `select-group` is configured +(`WSRCONFIG(ws)->n_group_list > 0`), eligible groups are the entries of +`ws->cert_groups[]` that also appear in `WSRCONFIG(ws)->group_list[]`; +otherwise (no `select-group` configured) every entry of `ws->cert_groups[]` +is eligible. If exactly one eligible group exists, the worker MUST set +`ws->groupname` to it and proceed with `SEC_AUTH_INIT` as if the client had +requested that group, completing authentication without an extra round trip. +If zero or more than one eligible groups exist, the "Please select your +group." prompt (and the cert-group fallback of REQ-AUTH-AUTH-006) is +unchanged. +**Strength:** MUST +**Status:** DERIVED +**Source:** src/worker-auth.c:1730-1738 +**Acceptance:** positive, local — `cert-group-oid` configured, no +`select-group` list, client certificate carries exactly one group (OU); a +first POST with no `` completes directly with +`` and a `Set-Cookie: webvpncontext=` header. Negative, +local — client certificate carries multiple groups (OUs) and no +`select-group` list is configured (or more than one of its groups matches a +configured `select-group` list); a first POST with no `` still +yields "Please select your group." and no session cookie. +**Links:** REQ-AUTH-AUTH-006, REQ-AUTH-AUTH-010 + ## AUTH — plain (`auth = plain[passwd=...,otp=...]`) ### REQ-AUTH-AUTH-011 — `plain[...]` requires at least one of `passwd=`/`otp=`; `vhost_init` fails closed without it diff --git a/src/worker-auth.c b/src/worker-auth.c index b58c6796..ddab1e87 100644 --- a/src/worker-auth.c +++ b/src/worker-auth.c @@ -239,6 +239,50 @@ static int resolve_selected_group(worker_st *ws, const char *group, return 0; } +/* If the client did not request a group, but the certificate carries + * exactly one group eligible for selection, select it directly into + * ws->groupname instead of prompting the client to pick one (#692). + * + * A certificate group is eligible if no select-group list is configured, + * or if it is one of the configured select-group entries. Returns 1 and + * sets ws->groupname if exactly one eligible group was found, otherwise + * returns 0 and leaves ws->groupname untouched. + */ +static int auto_select_cert_group(worker_st *ws) +{ + unsigned int i, j; + const char *candidate = NULL; + + for (i = 0; i < ws->cert_groups_size; i++) { + if (WSRCONFIG(ws)->n_group_list > 0) { + unsigned int found = 0; + + for (j = 0; j < WSRCONFIG(ws)->n_group_list; j++) { + if (strcmp(ws->cert_groups[i], + WSRCONFIG(ws)->group_list[j]) == 0) { + found = 1; + break; + } + } + + if (!found) + continue; + } + + if (candidate != NULL && + strcmp(candidate, ws->cert_groups[i]) != 0) + return 0; + + candidate = ws->cert_groups[i]; + } + + if (candidate == NULL) + return 0; + + strlcpy(ws->groupname, candidate, sizeof(ws->groupname)); + return 1; +} + int get_auth_handler2(worker_st *ws, unsigned int http_ver, const char *pmsg, unsigned int pcounter) { @@ -1730,11 +1774,18 @@ int post_auth_handler(worker_st *ws, unsigned int http_ver) if (def_group == 0 && ws->cert_groups_size > 0 && ws->groupname[0] == 0 && unlisted_groupname[0] == 0) { + if (auto_select_cert_group(ws) == 0) { + oclog(ws, LOG_HTTP_DEBUG, + "user has not selected a group"); + return get_auth_handler2( + ws, http_ver, + "Please select your group.", 0); + } + oclog(ws, LOG_HTTP_DEBUG, - "user has not selected a group"); - return get_auth_handler2( - ws, http_ver, - "Please select your group.", 0); + "auto-selected certificate group '%s'", + ws->groupname); + ireq.group_name = ws->groupname; } ireq.tls_auth_ok = ws->cert_auth_ok; diff --git a/tests/certs/user-group-single-cert.pem b/tests/certs/user-group-single-cert.pem new file mode 100644 index 00000000..017f2140 --- /dev/null +++ b/tests/certs/user-group-single-cert.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDXDCCAhSgAwIBAgIEaps0ATANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJD +QTAgFw0xMzA2MDYxMjUxMjlaGA85OTk5MTIzMTIzNTk1OVowKjESMBAGA1UECxMJ +b25seWdyb3VwMRQwEgYKCZImiZPyLGQBARMEdGVzdDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAN6qw04hFLOC2MRJ/D8+Cj5L3t8+8vDlPkRwyNdAFuaH +GPloxK+AiFGXM/sH578/SjogH1FzfcKWH2RXlsPY7WH5sZy6wifOJ4vxKMcPKzDX +RuPp/Nfte3vXpnuoTEwqHFXwjdgFb/RsgpRPKFyQACe0RmB6cEXkFk9m5nzWaX53 +KcfoYq9dfoQjQLodMf4ChS0Ig5EWM+9eukIACmN6lAyxkZ0T7lqj+yEG4Ab8hVvr +U2v3A0okDnfxTYSjZ2FOOBoRkrtdQGosDScfOLKEGwtHfPP0tf5epddCMiqy9rIq +ghU7TyjBRizaa5v1hbsHGG/O/r4bhAwMW387q0yyv8MCAwEAAaN1MHMwDAYDVR0T +AQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCBaAwHQYD +VR0OBBYEFNTtPyhLkhtMtVfBy+qKOOLovVgqMB8GA1UdIwQYMBaAFEgjNFMKiTE4 +SlrqyrbSpt7OHSsYMA0GCSqGSIb3DQEBCwUAA4IBMQBzkegY41jO5wFik/u6lzKH +Bz9UZMxKO21++MN5HOWIvY4+PaQIJl9ozF+DKCp1jFDq52llxRw4jFgiwSmy0vZ8 +StVgteq83+QFTWAiTw37t1dRW/30mdlqYSMd5/LZQ/s3sCt0q8fApa0jrzx6XQuZ +DAhUW4olcl9lBbC4CH929TpawgurBn1PTpBQQKoMugSPb8eGmL/lymSdmvSp4rbu +hU48hv6jaN8k9IcM5A72xGYODnaI5F3PHeXP9DzQRsyGnrVbIswJMDhVYFnSCA4c +SX2+bk4iY5Oh5HszdNorYDJ0/dNW0/ifYw7btfECZ/59t37DHP6kQU9RrGKXnJKj +XZ3DNGI3m0dZt5ZTSdrOD1/3APLyRiVomOHoaioVS0E58bh+OS5svTL9oMONhDSV +-----END CERTIFICATE----- diff --git a/tests/certs/user-group-single-cert.tmpl b/tests/certs/user-group-single-cert.tmpl new file mode 100644 index 00000000..9a4199d9 --- /dev/null +++ b/tests/certs/user-group-single-cert.tmpl @@ -0,0 +1,7 @@ +dn = "uid=test,ou=onlygroup" +tls_www_client +signing_key +encryption_key +expiration_days = -1 +activation_date = "2013-06-06 14:51:29" +serial = 0x6a9b3401 diff --git a/tests/data/test-group-cert-autoselect-filter.config b/tests/data/test-group-cert-autoselect-filter.config new file mode 100644 index 00000000..7b245f9a --- /dev/null +++ b/tests/data/test-group-cert-autoselect-filter.config @@ -0,0 +1,179 @@ +# User authentication method. Could be set multiple times and in that case +# all should succeed. +# Options: certificate, pam. +auth = "certificate" +#auth = "plain[@SRCDIR@/data/test-group.passwd]" +#auth = "pam" + +isolate-workers = @ISOLATE_WORKERS@ + +# A banner to be displayed on clients +#banner = "Welcome" + +# Use listen-host to limit to a specific IP or to the IPs of a provided hostname. +#listen-host = [IP|HOSTNAME] + +use-dbus = no + +# Limit the number of clients. Unset or set to zero for unlimited. +#max-clients = 1024 +max-clients = 16 + +# Limit the number of client connections to one every X milliseconds +# (X is the provided value). Set to zero for no limit. +#rate-limit-ms = 100 + +# Limit the number of identical clients (i.e., users connecting multiple times) +# Unset or set to zero for unlimited. +max-same-clients = 2 + +# TCP and UDP port number +tcp-port = @PORT@ +udp-port = @PORT@ + +# Keepalive in seconds +keepalive = 32400 + +# Dead peer detection in seconds +dpd = 440 + +# MTU discovery (DPD must be enabled) +try-mtu-discovery = false + +# The key and the certificates of the server +# The key may be a file, or any URL supported by GnuTLS (e.g., +# tpmkey:uuid=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx;storage=user +# or pkcs11:object=my-vpn-key;object-type=private) +# +# There may be multiple certificate and key pairs and each key +# should correspond to the preceding certificate. +server-cert = @SRCDIR@/certs/server-cert.pem +server-key = @SRCDIR@/certs/server-key.pem + +# Diffie-Hellman parameters. Only needed if you require support +# for the DHE ciphersuites (by default this server supports ECDHE). +# Can be generated using: +# certtool --generate-dh-params --outfile /path/to/dh.pem +#dh-params = /path/to/dh.pem + +# If you have a certificate from a CA that provides an OCSP +# service you may provide a fresh OCSP status response within +# the TLS handshake. That will prevent the client from connecting +# independently on the OCSP server. +# You can update this response periodically using: +# ocsptool --ask --load-cert=your_cert --load-issuer=your_ca --outfile response +# Make sure that you replace the following file in an atomic way. +#ocsp-response = /path/to/ocsp.der + +# In case PKCS #11 or TPM keys are used the PINs should be available +# in files. The srk-pin-file is applicable to TPM keys only (It's the storage +# root key). +#pin-file = /path/to/pin.txt +#srk-pin-file = /path/to/srkpin.txt + +# The Certificate Authority that will be used +# to verify clients if certificate authentication +# is set. +ca-cert = @SRCDIR@/certs/ca.pem + +# The object identifier that will be used to read the user ID in the client certificate. +# The object identifier should be part of the certificate's DN +# Useful OIDs are: +# CN = 2.5.4.3, UID = 0.9.2342.19200300.100.1.1 +cert-user-oid = 0.9.2342.19200300.100.1.1 + +# The object identifier that will be used to read the user group in the client +# certificate. The object identifier should be part of the certificate's DN +# Useful OIDs are: +# OU (organizational unit) = 2.5.4.11 +cert-group-oid = 2.5.4.11 + +# Only "onlygroup" is an eligible group. Used to test that auto_select_cert_group() +# correctly filters cert groups against the configured list. +select-group = onlygroup + +# A revocation list of ca-cert is set +#crl = /path/to/crl.pem + +# GnuTLS priority string +tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE:%COMPAT" + +# To enforce perfect forward secrecy (PFS) on the main channel. +#tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA" + +# The time (in seconds) that a client is allowed to stay connected prior +# to authentication +auth-timeout = 40 + +# Script to call when a client connects and obtains an IP +# Parameters are passed on the environment. +# REASON, USERNAME, GROUPNAME, HOSTNAME (the hostname selected by client), +# DEVICE, IP_REAL (the real IP of the client), IP_LOCAL (the local IP +# in the P-t-P connection), IP_REMOTE (the VPN IP of the client). REASON +# may be "connect" or "disconnect". +#connect-script = /usr/bin/myscript +#disconnect-script = /usr/bin/myscript + +# UTMP +use-utmp = true + +# PID file +pid-file = ./ocserv.pid + +# The default server directory. Does not require any devices present. +#chroot-dir = /path/to/chroot + +# socket file used for IPC, will be appended with .PID +# It must be accessible within the chroot environment (if any) +socket-file = ./ocserv-socket + +# The user the worker processes will be run as. It should be +# unique (no other services run as this user). +run-as-user = @USERNAME@ +run-as-group = @GROUP@ + +# Network settings + +device = vpns + +# The default domain to be advertised +default-domain = example.com + +ipv4-network = 192.168.1.0 +ipv4-netmask = 255.255.255.0 +# Use the keyword local to advertise the local P-t-P address as DNS server +ipv4-dns = 192.168.1.1 + +# The NBNS server (if any) +#ipv4-nbns = 192.168.2.3 + +#ipv6-address = +#ipv6-mask = +#ipv6-dns = + +# Prior to leasing any IP from the pool ping it to verify that +# it is not in use by another (unrelated to this server) host. +ping-leases = false + +# Leave empty to assign the default MTU of the device +# mtu = + +route = 192.168.1.0/255.255.255.0 +#route = 192.168.5.0/255.255.255.0 + +# +# The following options are for (experimental) AnyConnect client +# compatibility. They are only available if the server is built +# with --enable-anyconnect +# + +# Client profile xml. A sample file exists in doc/profile.xml. +# This file must be accessible from inside the worker's chroot. +# The profile is ignored by the openconnect client. +#user-profile = profile.xml + +# Unless set to false it is required for clients to present their +# certificate even if they are authenticating via a previously granted +# cookie. Legacy CISCO clients do not do that, and thus this option +# should be set for them. +cisco-client-compat = true diff --git a/tests/data/test-group-cert-autoselect.config b/tests/data/test-group-cert-autoselect.config new file mode 100644 index 00000000..97c9f2c3 --- /dev/null +++ b/tests/data/test-group-cert-autoselect.config @@ -0,0 +1,175 @@ +# User authentication method. Could be set multiple times and in that case +# all should succeed. +# Options: certificate, pam. +auth = "certificate" +#auth = "plain[@SRCDIR@/data/test-group.passwd]" +#auth = "pam" + +isolate-workers = @ISOLATE_WORKERS@ + +# A banner to be displayed on clients +#banner = "Welcome" + +# Use listen-host to limit to a specific IP or to the IPs of a provided hostname. +#listen-host = [IP|HOSTNAME] + +use-dbus = no + +# Limit the number of clients. Unset or set to zero for unlimited. +#max-clients = 1024 +max-clients = 16 + +# Limit the number of client connections to one every X milliseconds +# (X is the provided value). Set to zero for no limit. +#rate-limit-ms = 100 + +# Limit the number of identical clients (i.e., users connecting multiple times) +# Unset or set to zero for unlimited. +max-same-clients = 2 + +# TCP and UDP port number +tcp-port = @PORT@ +udp-port = @PORT@ + +# Keepalive in seconds +keepalive = 32400 + +# Dead peer detection in seconds +dpd = 440 + +# MTU discovery (DPD must be enabled) +try-mtu-discovery = false + +# The key and the certificates of the server +# The key may be a file, or any URL supported by GnuTLS (e.g., +# tpmkey:uuid=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx;storage=user +# or pkcs11:object=my-vpn-key;object-type=private) +# +# There may be multiple certificate and key pairs and each key +# should correspond to the preceding certificate. +server-cert = @SRCDIR@/certs/server-cert.pem +server-key = @SRCDIR@/certs/server-key.pem + +# Diffie-Hellman parameters. Only needed if you require support +# for the DHE ciphersuites (by default this server supports ECDHE). +# Can be generated using: +# certtool --generate-dh-params --outfile /path/to/dh.pem +#dh-params = /path/to/dh.pem + +# If you have a certificate from a CA that provides an OCSP +# service you may provide a fresh OCSP status response within +# the TLS handshake. That will prevent the client from connecting +# independently on the OCSP server. +# You can update this response periodically using: +# ocsptool --ask --load-cert=your_cert --load-issuer=your_ca --outfile response +# Make sure that you replace the following file in an atomic way. +#ocsp-response = /path/to/ocsp.der + +# In case PKCS #11 or TPM keys are used the PINs should be available +# in files. The srk-pin-file is applicable to TPM keys only (It's the storage +# root key). +#pin-file = /path/to/pin.txt +#srk-pin-file = /path/to/srkpin.txt + +# The Certificate Authority that will be used +# to verify clients if certificate authentication +# is set. +ca-cert = @SRCDIR@/certs/ca.pem + +# The object identifier that will be used to read the user ID in the client certificate. +# The object identifier should be part of the certificate's DN +# Useful OIDs are: +# CN = 2.5.4.3, UID = 0.9.2342.19200300.100.1.1 +cert-user-oid = 0.9.2342.19200300.100.1.1 + +# The object identifier that will be used to read the user group in the client +# certificate. The object identifier should be part of the certificate's DN +# Useful OIDs are: +# OU (organizational unit) = 2.5.4.11 +cert-group-oid = 2.5.4.11 + +# A revocation list of ca-cert is set +#crl = /path/to/crl.pem + +# GnuTLS priority string +tls-priorities = "PERFORMANCE:%SERVER_PRECEDENCE:%COMPAT" + +# To enforce perfect forward secrecy (PFS) on the main channel. +#tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA" + +# The time (in seconds) that a client is allowed to stay connected prior +# to authentication +auth-timeout = 40 + +# Script to call when a client connects and obtains an IP +# Parameters are passed on the environment. +# REASON, USERNAME, GROUPNAME, HOSTNAME (the hostname selected by client), +# DEVICE, IP_REAL (the real IP of the client), IP_LOCAL (the local IP +# in the P-t-P connection), IP_REMOTE (the VPN IP of the client). REASON +# may be "connect" or "disconnect". +#connect-script = /usr/bin/myscript +#disconnect-script = /usr/bin/myscript + +# UTMP +use-utmp = true + +# PID file +pid-file = ./ocserv.pid + +# The default server directory. Does not require any devices present. +#chroot-dir = /path/to/chroot + +# socket file used for IPC, will be appended with .PID +# It must be accessible within the chroot environment (if any) +socket-file = ./ocserv-socket + +# The user the worker processes will be run as. It should be +# unique (no other services run as this user). +run-as-user = @USERNAME@ +run-as-group = @GROUP@ + +# Network settings + +device = vpns + +# The default domain to be advertised +default-domain = example.com + +ipv4-network = 192.168.1.0 +ipv4-netmask = 255.255.255.0 +# Use the keyword local to advertise the local P-t-P address as DNS server +ipv4-dns = 192.168.1.1 + +# The NBNS server (if any) +#ipv4-nbns = 192.168.2.3 + +#ipv6-address = +#ipv6-mask = +#ipv6-dns = + +# Prior to leasing any IP from the pool ping it to verify that +# it is not in use by another (unrelated to this server) host. +ping-leases = false + +# Leave empty to assign the default MTU of the device +# mtu = + +route = 192.168.1.0/255.255.255.0 +#route = 192.168.5.0/255.255.255.0 + +# +# The following options are for (experimental) AnyConnect client +# compatibility. They are only available if the server is built +# with --enable-anyconnect +# + +# Client profile xml. A sample file exists in doc/profile.xml. +# This file must be accessible from inside the worker's chroot. +# The profile is ignored by the openconnect client. +#user-profile = profile.xml + +# Unless set to false it is required for clients to present their +# certificate even if they are authenticating via a previously granted +# cookie. Legacy CISCO clients do not do that, and thus this option +# should be set for them. +cisco-client-compat = true diff --git a/tests/meson.build b/tests/meson.build index 6c7a7f33..c9b17888 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -147,7 +147,8 @@ if have_cwrap 'haproxy-proxyproto', 'haproxy-proxyproto-v1', 'drain-server', 'drain-server-fail', 'test-ignore-querystring-of-post', - 'test-group-cert', 'test-fork', 'test-pass-svc', 'test-cert-svc', + 'test-group-cert', 'test-group-cert-autoselect', + 'test-fork', 'test-pass-svc', 'test-cert-svc', 'test-secmod-kill', 'test-syslog-facility', ] diff --git a/tests/test-group-cert-autoselect b/tests/test-group-cert-autoselect new file mode 100755 index 00000000..258aa71a --- /dev/null +++ b/tests/test-group-cert-autoselect @@ -0,0 +1,291 @@ +#!/bin/bash +# +# Copyright (C) 2026 Nikos Mavrogiannopoulos +# +# This file is part of ocserv. +# +# ocserv is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# ocserv is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GnuTLS; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +SERV="${SERV:-../src/ocserv}" +srcdir=${srcdir:-.} +NO_NEED_ROOT=1 +HEAD=$(mktemp) +POST=$(mktemp) +OUTFILE=$(mktemp) + +. `dirname $0`/common.sh + +eval "${GETPORT}" + +echo "Testing cert-group auto-selection (#692)" + +# Reproducer for #692: when cert-group-oid is configured and the client +# certificate carries exactly one group, and no "select-group" list is +# configured (so there is nothing to disambiguate), the server must not +# force an extra "select your group" round trip before completing +# authentication. + +function finish { + set +e + echo " * Cleaning up..." + test -n "${CONFIG}" && rm -f ${CONFIG} >/dev/null 2>&1 + rm -f $HEAD $POST $OUTFILE >/dev/null 2>&1 + cleanup +} +trap finish EXIT + +update_config test-group-cert-autoselect.config +launch_simple_sr_server -d 1 -f -c ${CONFIG} +PID=$! +wait_server $PID + +TARGET=https://$ADDRESS:$PORT + +cat >$HEAD <<_EOF +Accept-Encoding:identity +X-Transcend-Version:1 +X-Aggregate-Auth:1 +Connection:close +_EOF + +echo " * Sanity check: explicit group-select with a single certificate group" + +# Sanity check: explicitly selecting the (only) certificate group works +# and completes authentication immediately. +cat >$POST <<_EOF + + +onlygroup + +_EOF + +LD_PRELOAD=libsocket_wrapper.so curl -i -A 'OpenConnect VPN Agent v9.12' \ + -H @$HEAD -k \ + --cert ${srcdir}/certs/user-group-single-cert.pem \ + --key ${srcdir}/certs/user-group-key.pem \ + -d@$POST $TARGET >$OUTFILE + +grep 'auth id="success"' $OUTFILE >/dev/null +if test $? != 0; then + echo "Explicit group selection did not complete authentication" + echo "===========================================================" + cat $OUTFILE + exit 1 +fi + +grep "Set-Cookie: webvpncontext=" $OUTFILE >/dev/null +if test $? != 0; then + echo "Explicit group selection did not result in a session cookie" + echo "=============================================================" + cat $OUTFILE + exit 1 +fi + +echo " * OK: explicit group selection completed authentication" + +echo " * Reproducer: single eligible certificate group, no group-select sent" + +# The actual reproducer: the client certificate has exactly one group +# (OU=onlygroup) and the server has no "select-group" list configured +# (so the single certificate group is unambiguous). Authenticate without +# sending a at all, as a first-contact AnyConnect client +# would. Authentication must complete directly, without an extra +# "Please select your group" prompt. +cat >$POST <<_EOF + + +4.8.03036 + +_EOF + +LD_PRELOAD=libsocket_wrapper.so curl -i -A 'OpenConnect VPN Agent v9.12' \ + -H @$HEAD -k \ + --cert ${srcdir}/certs/user-group-single-cert.pem \ + --key ${srcdir}/certs/user-group-key.pem \ + -d@$POST $TARGET >$OUTFILE + +grep "Please select your group" $OUTFILE >/dev/null +if test $? = 0; then + echo "Server asked to select a group despite the certificate" + echo "containing a single, unambiguous group and no configured" + echo "select-group list (issue #692)" + echo "==========================================================" + cat $OUTFILE + exit 1 +fi + +grep 'auth id="success"' $OUTFILE >/dev/null +if test $? != 0; then + echo "Authentication did not complete with the certificate's" + echo "sole group automatically selected" + echo "========================================================" + cat $OUTFILE + exit 1 +fi + +grep "Set-Cookie: webvpncontext=" $OUTFILE >/dev/null +if test $? != 0; then + echo "Automatic group selection did not result in a session cookie" + echo "==============================================================" + cat $OUTFILE + exit 1 +fi + +echo " * OK: certificate's sole group was auto-selected, no prompt sent" + +echo " * Ambiguous case: multiple eligible certificate groups, no group-select sent" + +# Ambiguous case: the client certificate carries multiple groups +# (OU=group1, group2, group3, group4) and the server has no +# "select-group" list configured, so all of them are eligible and there +# is no single unambiguous group to auto-select. The server must still +# prompt the client to select a group. +cat >$POST <<_EOF + + +4.8.03036 + +_EOF + +LD_PRELOAD=libsocket_wrapper.so curl -i -A 'OpenConnect VPN Agent v9.12' \ + -H @$HEAD -k \ + --cert ${srcdir}/certs/user-group-cert.pem \ + --key ${srcdir}/certs/user-group-key.pem \ + -d@$POST $TARGET >$OUTFILE + +grep "Please select your group" $OUTFILE >/dev/null +if test $? != 0; then + echo "Server did not ask to select a group despite the certificate" + echo "containing multiple, ambiguous groups and no configured" + echo "select-group list" + echo "==========================================================" + cat $OUTFILE + exit 1 +fi + +# ocserv always sends a Set-Cookie: webvpncontext= header, even to clear +# a non-existing one (webvpncontext=; expires=...1970...). Only a +# non-empty cookie value indicates a completed session. +grep -E "Set-Cookie: webvpncontext=[^;[:space:]]" $OUTFILE >/dev/null +if test $? = 0; then + echo "Ambiguous group selection unexpectedly resulted in a session cookie" + echo "=====================================================================" + cat $OUTFILE + exit 1 +fi + +echo " * OK: ambiguous certificate groups still require group selection" + +# ----------------------------------------------------------------------- +# Restart the server with select-group = onlygroup to exercise the inner +# filtering loop in auto_select_cert_group(). +# ----------------------------------------------------------------------- +cleanup +mkdir -p $SOCKDIR + +echo "Testing cert-group auto-selection with select-group filter (#692)" + +update_config test-group-cert-autoselect-filter.config +launch_simple_sr_server -d 1 -f -c ${CONFIG} +PID=$! +wait_server $PID + +TARGET=https://$ADDRESS:$PORT + +echo " * Filtered match: cert group is in select-group list, no group-select sent" + +# select-group = onlygroup is configured; the client certificate carries +# exactly one group (OU=onlygroup) which is in the list. The eligible set +# has exactly one entry, so the server must auto-select it and complete +# authentication without an extra round trip. +cat >$POST <<_EOF + + +4.8.03036 + +_EOF + +LD_PRELOAD=libsocket_wrapper.so curl -i -A 'OpenConnect VPN Agent v9.12' \ + -H @$HEAD -k \ + --cert ${srcdir}/certs/user-group-single-cert.pem \ + --key ${srcdir}/certs/user-group-key.pem \ + -d@$POST $TARGET >$OUTFILE + +grep "Please select your group" $OUTFILE >/dev/null +if test $? = 0; then + echo "Server prompted for group despite cert's sole group being in" + echo "the configured select-group list (select-group filter path)" + echo "==========================================================" + cat $OUTFILE + exit 1 +fi + +grep 'auth id="success"' $OUTFILE >/dev/null +if test $? != 0; then + echo "Authentication did not complete with cert group auto-selected" + echo "from the configured select-group list" + echo "==============================================================" + cat $OUTFILE + exit 1 +fi + +grep "Set-Cookie: webvpncontext=" $OUTFILE >/dev/null +if test $? != 0; then + echo "Filtered auto-selection did not result in a session cookie" + echo "============================================================" + cat $OUTFILE + exit 1 +fi + +echo " * OK: cert group in select-group list was auto-selected" + +echo " * Filtered non-match: cert groups not in select-group list, no group-select sent" + +# select-group = onlygroup is configured; the client certificate carries +# four groups (OU=group1..4), none of which match "onlygroup". The eligible +# set is empty, so the server must fall back to the group selection prompt. +cat >$POST <<_EOF + + +4.8.03036 + +_EOF + +LD_PRELOAD=libsocket_wrapper.so curl -i -A 'OpenConnect VPN Agent v9.12' \ + -H @$HEAD -k \ + --cert ${srcdir}/certs/user-group-cert.pem \ + --key ${srcdir}/certs/user-group-key.pem \ + -d@$POST $TARGET >$OUTFILE + +grep "Please select your group" $OUTFILE >/dev/null +if test $? != 0; then + echo "Server did not prompt for group despite none of the cert's" + echo "groups matching the configured select-group list" + echo "==========================================================" + cat $OUTFILE + exit 1 +fi + +grep -E "Set-Cookie: webvpncontext=[^;[:space:]]" $OUTFILE >/dev/null +if test $? = 0; then + echo "Non-matching cert groups unexpectedly resulted in a session cookie" + echo "===================================================================" + cat $OUTFILE + exit 1 +fi + +echo " * OK: cert groups not in select-group list still require group selection" + +exit 0