Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/tests/system/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ 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/krishnavema/sssd-test-framework@multi-token-smart-card-support

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This change introduces a dependency on a personal fork (krishnavema/sssd-test-framework). While this might be acceptable for development, it poses a security and maintenance risk for the main branch. The changes from this fork should be merged into the upstream SSSD/sssd-test-framework repository, and the dependency should point to an official release or commit from the upstream repository before this pull request is merged.

git+https://github.com/SSSD/sssd-test-framework

160 changes: 160 additions & 0 deletions src/tests/system/tests/test_smartcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,78 @@

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

TOKEN1_LABEL = "SC_Token_1"
TOKEN2_LABEL = "SC_Token_2"
TOKEN_PIN = "123456"
Comment on lines +14 to +16

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To improve readability and maintainability of the polling loop in authenticate_with_smartcard, it's good practice to define the magic numbers used there as constants here.

TOKEN1_LABEL = "SC_Token_1"
TOKEN2_LABEL = "SC_Token_2"
TOKEN_PIN = "123456"

# Constants for the user resolvability polling loop
USER_RESOLVABLE_ATTEMPTS = 15
USER_RESOLVABLE_INTERVAL_S = 2
USER_RESOLVABLE_CACHE_EXPIRY_ATTEMPT = 3



def enroll_to_token(
client: Client,
ipa: IPA,
username: str,
*,
token_label: str,
cert_id: str = "01",
pin: str = TOKEN_PIN,
) -> None:
"""
Request an IPA-signed certificate for *username* and store it on *token_label*.

:param client: Client role object.
:type client: Client
:param ipa: IPA role object whose CA issues the certificate.
:type ipa: IPA
:param username: IPA principal to issue the certificate for.
:type username: str
:param token_label: SoftHSM token label to write the objects to.
:type token_label: str
:param cert_id: PKCS#11 object ID, defaults to "01".
:type cert_id: str, optional
:param pin: User PIN for the token, defaults to TOKEN_PIN.
:type pin: str, optional
"""
cert, key, _ = ipa.ca.request(username)
cert_content = ipa.fs.read(cert)
key_content = ipa.fs.read(key)

cert_path = f"/opt/test_ca/{username}_{token_label}.crt"
key_path = f"/opt/test_ca/{username}_{token_label}.key"

client.fs.write(cert_path, cert_content)
client.fs.write(key_path, key_content)

client.smartcard.add_key(key_path, key_id=cert_id, pin=pin, token_label=token_label, label=username)
client.smartcard.add_cert(cert_path, cert_id=cert_id, pin=pin, token_label=token_label, label=username)


def setup_two_tokens(
client: Client,
ipa: IPA,
*,
token1_username: str,
token2_username: str,
) -> None:
"""
Create two SoftHSM tokens, each holding an IPA-signed certificate.

:param client: Client role object.
:type client: Client
:param ipa: IPA role object.
:type ipa: IPA
:param token1_username: IPA user whose cert goes onto token 1.
:type token1_username: str
:param token2_username: IPA user whose cert goes onto token 2.
:type token2_username: str
"""
client.smartcard.initialize_card(label=TOKEN1_LABEL, user_pin=TOKEN_PIN)
enroll_to_token(client, ipa, token1_username, token_label=TOKEN1_LABEL)

client.smartcard.initialize_card(label=TOKEN2_LABEL, user_pin=TOKEN_PIN, reset=False)
enroll_to_token(client, ipa, token2_username, token_label=TOKEN2_LABEL)


@pytest.mark.importance("critical")
@pytest.mark.topology(KnownTopology.Client)
Expand All @@ -30,3 +100,93 @@ def test_smartcard__su_as_local_user(client: Client):
result = client.host.conn.run("su - localuser1 -c 'su - localuser1 -c whoami'", input="123456")
assert "PIN" in result.stderr, "String 'PIN' was not found in stderr!"
assert "localuser1" in result.stdout, "'localuser1' not found in 'whoami' output!"


@pytest.mark.importance("critical")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__two_tokens_match_on_first(client: Client, ipa: IPA):
"""
:title: Two smart cards – valid certificate on the first token
:setup:
1. Create IPA user and a decoy IPA user
2. Initialize two SoftHSM tokens (simulating two smart cards)
3. Place the target user's IPA certificate on token 1
4. Place the decoy user's IPA certificate on token 2
5. Configure SSSD for smart card authentication and start services
:steps:
1. Authenticate as the target IPA user via nested ``su`` with the
smart card PIN
:expectedresults:
1. SSSD's ``p11_child`` finds valid certificates on both tokens,
SSSD maps the token-1 certificate to the target user, prompts
for PIN, and authentication succeeds
:customerscenario: True
"""
username = "scuser_t1"
decoy = "scdecoy_t1"
ipa.user(username).add()
ipa.user(decoy).add()

setup_two_tokens(client, ipa, token1_username=username, token2_username=decoy)
client.sssd.common.smartcard_with_softhsm(client.smartcard)
assert client.auth.su.smartcard_with_su(username, TOKEN_PIN)


@pytest.mark.importance("critical")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__two_tokens_match_on_second(client: Client, ipa: IPA):
"""
:title: Two smart cards – valid certificate only on the second token
:setup:
1. Create IPA user and a decoy IPA user
2. Initialize two SoftHSM tokens (simulating two smart cards)
3. Place the decoy user's IPA certificate on token 1
4. Place the target user's IPA certificate on token 2
5. Configure SSSD for smart card authentication and start services
:steps:
1. Authenticate as the target IPA user via nested ``su`` with the
smart card PIN
:expectedresults:
1. SSSD's ``p11_child`` does **not** stop at token 1 (whose cert
maps to the decoy user); it continues to token 2, finds the
certificate that maps to the target user, prompts for PIN, and
authentication succeeds
:customerscenario: True
"""
username = "scuser_t2"
decoy = "scdecoy_t2"
ipa.user(username).add()
ipa.user(decoy).add()

setup_two_tokens(client, ipa, token1_username=decoy, token2_username=username)
client.sssd.common.smartcard_with_softhsm(client.smartcard)
assert client.auth.su.smartcard_with_su(username, TOKEN_PIN)


@pytest.mark.importance("critical")
@pytest.mark.topology(KnownTopology.IPA)
@pytest.mark.builtwith(client="virtualsmartcard")
def test_smartcard__two_tokens_match_on_both(client: Client, ipa: IPA):
"""
:title: Two smart cards – valid certificate on both tokens
:setup:
1. Create IPA user
2. Initialize two SoftHSM tokens (simulating two smart cards)
3. Place a valid IPA certificate for the same user on both tokens
4. Configure SSSD for smart card authentication and start services
:steps:
1. Authenticate as the IPA user via nested ``su`` with the PIN of
the first token
:expectedresults:
1. SSSD's ``p11_child`` finds valid certificates on both tokens and
authentication succeeds regardless of which token is tried first
:customerscenario: True
"""
username = "scuser_both"
ipa.user(username).add()

setup_two_tokens(client, ipa, token1_username=username, token2_username=username)
client.sssd.common.smartcard_with_softhsm(client.smartcard)
assert client.auth.su.smartcard_with_su(username, TOKEN_PIN, num_certs=2)
Loading