Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions src/p11_child/p11_child_openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
#include "util/crypto/sss_crypto.h"
#include "p11_child/p11_child.h"

/* Timeout in seconds for soft_ocsp OCSP requests when no explicit timeout is set. */
#define SOFT_OCSP_DEFAULT_TIMEOUT 10

struct p11_ctx {
X509_STORE *x509_store;
const char *ca_db;
Expand Down Expand Up @@ -381,8 +384,18 @@ static errno_t do_ocsp(struct p11_ctx *p11_ctx, X509 *cert)

OCSP_request_add1_nonce(ocsp_req, NULL, -1);

if (p11_ctx->ocsp_deadline != -1 && p11_ctx->cert_verify_opts->soft_ocsp) {
req_timeout = p11_ctx->ocsp_deadline - time(NULL);
if (p11_ctx->cert_verify_opts->soft_ocsp) {
if (p11_ctx->ocsp_deadline != -1) {
req_timeout = p11_ctx->ocsp_deadline - time(NULL);
} else {
/* No explicit timeout was provided but soft_ocsp is enabled.
* Use a default timeout to prevent the BIO connect from
* blocking indefinitely when the OCSP host is unreachable. */
DEBUG(SSSDBG_TRACE_INTERNAL,
"No timeout set with soft_ocsp, using default [%d]s.\n",
SOFT_OCSP_DEFAULT_TIMEOUT);
req_timeout = SOFT_OCSP_DEFAULT_TIMEOUT;
}
if (req_timeout <= 0) {
/* no time left for OCSP */
DEBUG(SSSDBG_TRACE_INTERNAL,
Expand Down Expand Up @@ -594,15 +607,18 @@ errno_t init_p11_ctx(TALLOC_CTX *mem_ctx, const char *ca_db,
return ENOMEM;
}

if (timeout == 1) {
/* timeout of 1 sec is too short (see -1 in deadline calculation),
* increasing to 2 and hope that the ocsp operation finishes
* before p11_child is terminated.
*/
timeout = 2;
/* Use half the total timeout for OCSP to leave p11_child enough margin
* to output results before the PAM responder's kill timer fires (RHEL-5043). */
if (timeout > 0) {
time_t ocsp_budget = timeout / 2;
if (ocsp_budget < 1) {
ocsp_budget = 1;
}
ctx->ocsp_deadline = time(NULL) + ocsp_budget;
} else {
/* timeout <= 0 means no timeout specified */
ctx->ocsp_deadline = -1;
}
/* timeout <= 0 means no timeout specified */
ctx->ocsp_deadline = timeout > 0 ? time(NULL) + timeout - 1 : -1;

/* See https://wiki.openssl.org/index.php/Library_Initialization for
* details. */
Expand Down
3 changes: 2 additions & 1 deletion src/tests/system/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ git+https://github.com/next-actions/pytest-mh
git+https://github.com/next-actions/pytest-ticket
git+https://github.com/next-actions/pytest-tier
git+https://github.com/next-actions/pytest-output
git+https://github.com/SSSD/sssd-test-framework
#git+https://github.com/SSSD/sssd-test-framework
git+https://github.com/krishnavema/sssd-test-framework@smartcard-pkcs11
212 changes: 212 additions & 0 deletions src/tests/system/tests/test_smartcard_soft_ocsp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""
SSSD smart card authentication with soft_ocsp

Regression tests for RHEL-5043: when ``soft_ocsp`` is configured and the OCSP
responder is unreachable, smart card authentication should still succeed.

:requirement: smartcard_authentication
"""

from __future__ import annotations

import time

import pytest
from sssd_test_framework.roles.client import Client
from sssd_test_framework.roles.ipa import IPA
from sssd_test_framework.topology import KnownTopology

#: Seconds to wait after restarting virt_cacard so that pcscd registers
#: the virtual card reader and the card is fully presented.
VIRT_CACARD_SETTLE_SECONDS = 5

#: Label for PKCS#11 objects stored in SoftHSM. ``p11_child``'s
#: ``parse_p11_child_response()`` rejects certificates that have an
#: empty label field, so every object must carry a non-empty one.
PKCS11_LABEL = "cert"


def _enroll_smartcard(client: Client, ipa: IPA, username: str) -> None:
"""Request a certificate from the IPA CA and load it into a SoftHSM token.

Follows the same ordering as
``test_ipa__switch_user_with_smartcard_authentication`` which is the
canonical IPA smart-card test.
"""
cert, key, _ = ipa.ca.request(username)

cert_content = ipa.fs.read(cert)
key_content = ipa.fs.read(key)
client.fs.write(f"/opt/test_ca/{username}.crt", cert_content)
client.fs.write(f"/opt/test_ca/{username}.key", key_content)

client.smartcard.initialize_card()
client.smartcard.add_key(f"/opt/test_ca/{username}.key", label=PKCS11_LABEL)
client.smartcard.add_cert(f"/opt/test_ca/{username}.crt", label=PKCS11_LABEL)


def _configure_smartcard_and_start(
client: Client,
*,
certificate_verification: str | None = None,
p11_child_timeout: int = 30,
) -> None:
"""Configure SSSD for smart-card authentication and present a virtual card."""
client.authselect.select("sssd", ["with-smartcard"])

if certificate_verification is not None:
client.sssd.sssd["certificate_verification"] = certificate_verification

client.sssd.pam["pam_cert_auth"] = "True"
client.sssd.pam["p11_child_timeout"] = str(p11_child_timeout)

client.sssd.start()
client.svc.restart("virt_cacard.service")
time.sleep(VIRT_CACARD_SETTLE_SECONDS)


def _assert_smartcard_auth_success(client: Client, username: str) -> None:
"""Run the double-``su`` pattern and verify PIN-based authentication."""
result = client.host.conn.run(
f"su - {username} -c 'su - {username} -c whoami'",
input="123456",
)
assert "PIN" in result.stderr, f"String 'PIN' was not found in stderr! Stderr content: {result.stderr}"
assert username in result.stdout, f"'{username}' not found in 'whoami' output! Stdout content: {result.stdout}"


def _redirect_ocsp_responder(client: Client, ipa: IPA, target_ip: str) -> None:
"""Point the IPA OCSP responder hostname to *target_ip* via ``/etc/hosts``."""
ipa_ca_hostname = f"ipa-ca.{ipa.domain}"
client.fs.append("/etc/hosts", f"\n{target_ip} {ipa_ca_hostname}\n")


@pytest.mark.ticket(jira="RHEL-5043")
@pytest.mark.importance("high")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__soft_ocsp_with_unreachable_responder(client: Client, ipa: IPA):
"""
:title: Smart card authentication succeeds with soft_ocsp when OCSP responder is unreachable
:setup:
1. Create an IPA user and enroll a smart card.
2. Configure ``certificate_verification = soft_ocsp``.
3. Point ipa-ca to 192.168.123.1 (non-routable, packets silently dropped).
4. Start SSSD and present the virtual smart card.
:steps:
1. Authenticate via ``su`` with the smart card PIN.
:expectedresults:
1. PIN prompt appears and authentication succeeds despite the
unreachable OCSP responder.
:customerscenario: True
"""
username = "smartcarduser1"

ipa.user(username).add()
_enroll_smartcard(client, ipa, username)

# 192.168.123.1: non-routable → TCP SYN silently dropped → triggers the bug
_redirect_ocsp_responder(client, ipa, "192.168.123.1")
_configure_smartcard_and_start(client, certificate_verification="soft_ocsp")

_assert_smartcard_auth_success(client, username)


@pytest.mark.ticket(jira="RHEL-5043")
@pytest.mark.importance("high")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__soft_ocsp_with_reachable_responder(client: Client, ipa: IPA):
"""
:title: Smart card authentication succeeds with soft_ocsp when OCSP responder is reachable
:setup:
1. Create an IPA user and enroll a smart card.
2. Configure ``certificate_verification = soft_ocsp``.
3. Start SSSD and present the virtual smart card (OCSP responder is reachable).
:steps:
1. Authenticate via ``su`` with the smart card PIN.
:expectedresults:
1. PIN prompt appears and authentication succeeds; the OCSP check
completes normally.
:customerscenario: True
"""
username = "smartcarduser2"

ipa.user(username).add()
_enroll_smartcard(client, ipa, username)

# No /etc/hosts redirect – OCSP responder is reachable.
_configure_smartcard_and_start(client, certificate_verification="soft_ocsp")

_assert_smartcard_auth_success(client, username)


@pytest.mark.ticket(jira="RHEL-5043")
@pytest.mark.importance("high")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__soft_ocsp_with_connection_refused(client: Client, ipa: IPA):
"""
:title: Smart card authentication succeeds with soft_ocsp when OCSP connection is refused
:setup:
1. Create an IPA user and enroll a smart card.
2. Configure ``certificate_verification = soft_ocsp``.
3. Point ipa-ca to 127.0.0.7 (loopback, immediate TCP RST).
4. Start SSSD and present the virtual smart card.
:steps:
1. Authenticate via ``su`` with the smart card PIN.
:expectedresults:
1. PIN prompt appears and authentication succeeds; the OCSP
connection is immediately refused and soft_ocsp skips the check.
:customerscenario: True
"""
username = "smartcarduser3"

ipa.user(username).add()
_enroll_smartcard(client, ipa, username)

# 127.0.0.7: loopback → immediate connection-refused (TCP RST)
_redirect_ocsp_responder(client, ipa, "127.0.0.7")
_configure_smartcard_and_start(client, certificate_verification="soft_ocsp")

_assert_smartcard_auth_success(client, username)


@pytest.mark.ticket(jira="RHEL-5043")
@pytest.mark.importance("high")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__without_soft_ocsp_with_unreachable_responder(client: Client, ipa: IPA):
"""
:title: Smart card authentication fails without soft_ocsp when OCSP responder is unreachable
:setup:
1. Create an IPA user and enroll a smart card.
2. Do NOT set ``certificate_verification`` (default OCSP behaviour).
3. Point ipa-ca to 192.168.123.1 (unreachable).
4. Start SSSD and present the virtual smart card.
:steps:
1. Attempt to authenticate via ``su`` with the smart card PIN.
:expectedresults:
1. Without ``soft_ocsp``, the certificate check fails because the
OCSP responder is unreachable. The user sees a password prompt
(not a PIN prompt) or the authentication fails outright.
:customerscenario: True
"""
username = "smartcarduser4"

ipa.user(username).add()
_enroll_smartcard(client, ipa, username)

_redirect_ocsp_responder(client, ipa, "192.168.123.1")
# certificate_verification is left unset → default OCSP is enforced.
_configure_smartcard_and_start(client, certificate_verification=None)

result = client.host.conn.run(
f"su - {username} -c 'su - {username} -c whoami'",
input="123456",
raise_on_error=False,
)

assert (
"PIN" not in result.stderr or result.rc != 0
), f"Expected authentication to fail without soft_ocsp when OCSP is unreachable! rc={result.rc}"
Loading