Skip to content

Commit 5f65046

Browse files
committed
Adding support for virtual passkey functionality
1 parent 3f65596 commit 5f65046

File tree

6 files changed

+306
-0
lines changed

6 files changed

+306
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
test_venv = /opt/test_venv
3+
test_venv_bin = "f{test_venv}/bin"

sssd_test_framework/roles/client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ..utils.sss_override import SSSOverrideUtils
1818
from ..utils.sssctl import SSSCTLUtils
1919
from ..utils.sssd import SSSDUtils
20+
from ..utils.vfido import Vfido
2021
from .base import BaseLinuxRole
2122

2223
__all__ = [
@@ -104,6 +105,11 @@ def __init__(self, *args, **kwargs) -> None:
104105
Managing GDM interface from SCAutolib
105106
"""
106107

108+
self.vfido: Vfido = Vfido(self.host)
109+
"""
110+
Managing virtual passkey device and service
111+
"""
112+
107113
def setup(self) -> None:
108114
"""
109115
Called before execution of each test.

sssd_test_framework/roles/ipa.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
to_list_of_strings,
2626
to_list_without_none,
2727
)
28+
from ..roles.client import Client
2829
from ..utils.sssctl import SSSCTLUtils
2930
from ..utils.sssd import SSSDUtils
31+
from ..misc.globals import test_venv_bin
3032
from .base import BaseLinuxRole, BaseObject
3133
from .generic import GenericNetgroupMember, GenericPasswordPolicy
3234
from .nfs import NFSExport
@@ -1103,6 +1105,64 @@ def passkey_remove(self, passkey_mapping: str) -> IPAUser:
11031105
self._exec("remove-passkey", [passkey_mapping])
11041106
return self
11051107

1108+
def vfido_passkey_add_register(
1109+
self,
1110+
client: Client,
1111+
*,
1112+
pin: str | int | None = None,
1113+
) -> str:
1114+
"""
1115+
Register user passkey when using virtual-fido
1116+
"""
1117+
1118+
if pin is None:
1119+
pin = "empty"
1120+
1121+
client.host.conn.exec(["kinit", f"{self.host.adminuser}@{self.host.realm}"], input=self.host.adminpw)
1122+
1123+
result = client.host.conn.expect(
1124+
f"""
1125+
set pin "{pin}"
1126+
set timeout 60
1127+
1128+
spawn ipa user-add-passkey {self.name} --register
1129+
set ID_reg $spawn_id
1130+
1131+
if {{ ($pin ne "empty") }} {{
1132+
expect {{
1133+
-i $ID_reg -re "Enter PIN:*" {{}}
1134+
-i $ID_reg timeout {{puts "expect result: Unexpected output"; exit 201}}
1135+
-i $ID_reg eof {{puts "expect result: Unexpected end of file"; exit 202}}
1136+
}}
1137+
1138+
puts "Entering PIN\n"
1139+
send -i $ID_reg "{pin}\r"
1140+
}}
1141+
1142+
expect {{
1143+
-i $ID_reg -re "Please touch the device.*" {{}}
1144+
-i $ID_reg timeout {{puts "expect result: Unexpected output"; exit 203}}
1145+
-i $ID_reg eof {{puts "expect result: Unexpected end of file"; exit 204}}
1146+
}}
1147+
1148+
puts "Touching device"
1149+
spawn {test_venv_bin}/vfido_touch
1150+
set ID_touch $spawn_id
1151+
1152+
expect {{
1153+
-i $ID_reg -re "Added passkey mappings.*" {{}}
1154+
-i $ID_reg timeout {{puts "expect result: Unexpected output"; exit 205}}
1155+
-i $ID_reg eof {{puts "expect result: Unexpected end of file"; exit 206}}
1156+
}}
1157+
1158+
expect -i $ID_reg eof
1159+
expect -i $ID_touch eof
1160+
""",
1161+
raise_on_error=True,
1162+
)
1163+
1164+
return result.stdout_lines[-1].strip()
1165+
11061166
def iduseroverride(self) -> IDUserOverride:
11071167
"""
11081168
Add override to the IPA user.

sssd_test_framework/utils/authentication.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pytest_mh.utils.fs import LinuxFileSystem
1212

1313
from ..misc.errors import ExpectScriptError
14+
from ..misc.globals import test_venv_bin
1415
from .idp import IdpAuthenticationUtils
1516

1617
__all__ = [
@@ -639,6 +640,189 @@ def passkey(
639640
)
640641
return rc == 0
641642

643+
def vfido_passkey_with_output(
644+
self,
645+
username: str,
646+
*,
647+
pin: str | int | None = None,
648+
interactive_prompt: str = "Insert your passkey device, then press ENTER.",
649+
touch_prompt: str = "Please touch the device.",
650+
command: str = "exit 0",
651+
auth_method: PasskeyAuthenticationUseCases = PasskeyAuthenticationUseCases.PASSKEY_PIN,
652+
) -> tuple[int, int, str, str]:
653+
"""
654+
Call ``su - $username`` and authenticate the user with vfido passkey
655+
656+
:param username: Username
657+
:type username: str
658+
:param pin: Passkey PIN, defaults to None
659+
:type pin: str | int | None
660+
:param interactive_prompt: Interactive prompt, defaults to "Insert your passkey device, then press ENTER."
661+
:type interactive_prompt: str
662+
:param touch_prompt: Touch prompt, defaults to "Please touch the device."
663+
:type touch_prompt: str
664+
:param command: Command executed after user is authenticated, defaults to "exit 0"
665+
:type command: str
666+
:param auth_method: Authentication method, defaults to PasskeyAuthenticationUseCases.PASSKEY_PIN
667+
:type auth_method: PasskeyAuthenticationUseCases
668+
:return: Tuple containing [return code, command code, stdout, stderr].
669+
:rtype: Tuple[int, int, str, str]
670+
"""
671+
672+
match auth_method:
673+
case PasskeyAuthenticationUseCases.PASSKEY_PIN | \
674+
PasskeyAuthenticationUseCases.PASSKEY_PIN_AND_PROMPTS:
675+
if pin is None:
676+
raise ValueError(f"PIN is required for {str(auth_method)}")
677+
case PasskeyAuthenticationUseCases.PASSKEY_PROMPTS_NO_PIN | \
678+
PasskeyAuthenticationUseCases.PASSKEY_FALLBACK_TO_PASSWORD | \
679+
PasskeyAuthenticationUseCases.PASSKEY_NO_PIN_NO_PROMPTS:
680+
if pin is not None:
681+
raise ValueError(f"PIN is not required for {str(auth_method)}")
682+
683+
run_su = self.fs.mktmp(
684+
rf"""
685+
#!/bin/bash
686+
set -ex
687+
su --shell /bin/sh nobody -c "su - '{username}' -c '{command}'"
688+
""",
689+
mode="a=rx",
690+
)
691+
692+
result = self.host.conn.expect(
693+
rf"""
694+
# Disable debug output
695+
# exp_internal 0
696+
697+
proc exitmsg {{ msg code }} {{
698+
# Close spawned program, if we are in the prompt
699+
catch close
700+
701+
# Wait for the exit code
702+
lassign [wait] pid spawnid os_error_flag rc
703+
704+
puts ""
705+
puts "expect result: $msg"
706+
puts "expect exit code: $code"
707+
puts "expect spawn exit code: $rc"
708+
exit $code
709+
}}
710+
711+
# It takes some time to get authentication failure
712+
set timeout {DEFAULT_AUTHENTICATION_TIMEOUT}
713+
set prompt "\n.*\[#\$>\] $"
714+
set command "{command}"
715+
set auth_method "{auth_method}"
716+
717+
spawn "{run_su}"
718+
set ID_su $spawn_id
719+
720+
# If the authentication method set without entering the PIN, it will directly ask
721+
# prompt, if we set prompting options in sssd.conf it will ask interactive and touch prompt.
722+
723+
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_NO_PIN_NO_PROMPTS}")
724+
|| ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PROMPTS_NO_PIN}") }} {{
725+
expect {{
726+
-i $ID_su -re "{interactive_prompt}*" {{ send -i $ID_su "\n" }}
727+
-i $ID_su timeout {{exitmsg "Unexpected output" 201 }}
728+
-i $ID_su eof {{exitmsg "Unexpected end of file" 202 }}
729+
}}
730+
# If prompt options are set
731+
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PROMPTS_NO_PIN}") }} {{
732+
expect {{
733+
-i $ID_su -re "{touch_prompt}*" {{ }}
734+
-i $ID_su timeout {{exitmsg "Unexpected output" 203 }}
735+
-i $ID_su eof {{exitmsg "Unexpected end of file" 204 }}
736+
}}
737+
}}
738+
}}
739+
740+
# If authentication method set with PIN, after interactive prompt always ask to Enter the PIN.
741+
# If PIN is correct with prompt options in sssd.conf it will ask interactive and touch prompt.
742+
# If we press Enter key for PIN, sssd will fallback to next auth method, here it will ask
743+
# for Password.
744+
745+
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PIN}")
746+
|| ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PIN_AND_PROMPTS}")
747+
|| ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_FALLBACK_TO_PASSWORD}")}} {{
748+
expect {{
749+
-i $ID_su -re "{interactive_prompt}*" {{ send -i $ID_su "\n" }}
750+
-i $ID_su timeout {{exitmsg "Unexpected output" 205 }}
751+
-i $ID_su eof {{exitmsg "Unexpected end of file" 206 }}
752+
}}
753+
expect {{
754+
-i $ID_su -re "Enter PIN:*" {{send -i $ID_su "{pin}\r"}}
755+
-i $ID_su timeout {{exitmsg "Unexpected output" 207}}
756+
-i $ID_su eof {{exitmsg "Unexpected end of file" 208}}
757+
}}
758+
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_FALLBACK_TO_PASSWORD}") }} {{
759+
expect {{
760+
-i $ID_su -re "Password:*" {{send -i $ID_su "Secret123\r"}}
761+
-i $ID_su timeout {{exitmsg "Unexpected output" 209}}
762+
-i $ID_su eof {{exitmsg "Unexpected end of file" 210}}
763+
}}
764+
}}
765+
if {{ ($auth_method eq "{PasskeyAuthenticationUseCases.PASSKEY_PIN_AND_PROMPTS}") }} {{
766+
expect {{
767+
-i $ID_su -re "{touch_prompt}*" {{ }}
768+
-i $ID_su timeout {{exitmsg "Unexpected output" 211 }}
769+
-i $ID_su eof {{exitmsg "Unexpected end of file" 212 }}
770+
}}
771+
}}
772+
}}
773+
774+
# Now simulate touch on vfido device
775+
spawn {test_venv_bin}/vfido_touch
776+
set ID_touch $spawn_id
777+
778+
expect {{
779+
-i $ID_su -re "Authentication failure" {{exitmsg "Authentication failure" 1}}
780+
-i $ID_su eof {{exitmsg "Passkey authentication successful" 0}}
781+
-i $ID_su timeout {{exitmsg "Unexpected output" 213}}
782+
}}
783+
784+
expect -i $ID_touch eof
785+
786+
exitmsg "Unexpected code path" 220
787+
""",
788+
verbose=False,
789+
)
790+
791+
if result.rc > 200:
792+
raise ExpectScriptError(result.rc)
793+
794+
expect_data = result.stdout_lines[-3:]
795+
796+
# Get command exit code.
797+
cmdrc = int(expect_data[2].split(":")[1].strip())
798+
799+
# Alter stdout, first line is spawned command, the last three are our expect output.
800+
stdout = "\n".join(result.stdout_lines[1:-3])
801+
802+
return result.rc, cmdrc, stdout, result.stderr
803+
804+
def vfido_passkey(
805+
self,
806+
username: str,
807+
*,
808+
pin: str | int | None = None,
809+
command: str = "exit 0",
810+
) -> bool:
811+
"""
812+
Call ``su - $username`` and authenticate the user with passkey.
813+
814+
:param username: Username
815+
:type username: str
816+
:param pin: Passkey PIN.
817+
:type pin: str | int | None
818+
:param command: Command executed after user is authenticated, defaults to "exit 0"
819+
:type command: str
820+
:return: True if authentication was successful, False otherwise.
821+
:rtype: bool
822+
"""
823+
rc, _, _, _ = self.vfido_passkey_with_output(username=username, pin=pin, command=command)
824+
return rc == 0
825+
642826

643827
class SSHAuthenticationUtils(MultihostUtility[MultihostHost]):
644828
"""

sssd_test_framework/utils/gdm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from pytest_mh import MultihostHost, MultihostUtility
1111

12+
from ..utils.authentication import PasskeyAuthenticationUseCases
13+
1214
if TYPE_CHECKING:
1315
from ..roles.client import Client
1416

sssd_test_framework/utils/vfido.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Manage virtual FIDO device."""
2+
3+
from __future__ import annotations
4+
5+
from ..misc.globals import test_venv_bin
6+
from pytest_mh import MultihostHost, MultihostUtility
7+
8+
__all__ = [
9+
"Vfido",
10+
]
11+
12+
13+
class Vfido(MultihostUtility[MultihostHost]):
14+
"""
15+
Manage virtual passkey device and service
16+
"""
17+
18+
def __init__(self, host: MultihostHost):
19+
super().__init__(host)
20+
21+
def stop(self) -> None:
22+
"""Stop vfido service"""
23+
self.host.conn.exec(["systemctl", "stop", "vfido"])
24+
25+
def start(self) -> None:
26+
"""Start vfido service"""
27+
self.host.conn.exec(["systemctl", "start", "vfido"])
28+
29+
def reset(self) -> None:
30+
"""reset state of vfido service back to clean"""
31+
self.stop()
32+
self.host.conn.exec([f"{test_venv_bin}/vfido_reset"])
33+
34+
def touch(self) -> bool:
35+
"""
36+
send touch signal to virtual passkey
37+
"""
38+
result = self.host.conn.exec([f"{test_venv_bin}/vfido_touch"])
39+
return result.rc == 0
40+
41+
def pin_set(self, pin: str | int) -> None:
42+
"""Set pin on virtual passkey"""
43+
self.host.conn.exec([f"{test_venv_bin}/vfido_pin_set", str(pin)])
44+
45+
def pin_enable(self) -> None:
46+
"""Set pin on virtual passkey"""
47+
self.host.conn.exec([f"{test_venv_bin}/vfido_pin_enable"])
48+
49+
def pin_disable(self) -> None:
50+
"""Set pin on virtual passkey"""
51+
self.host.conn.exec([f"{test_venv_bin}/vfido_pin_disable"])

0 commit comments

Comments
 (0)