Skip to content

Commit ba6250b

Browse files
committed
Adding support for virtual passkey functionality
1 parent 65963d1 commit ba6250b

File tree

7 files changed

+342
-5
lines changed

7 files changed

+342
-5
lines changed

sssd_test_framework/hosts/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def features(self) -> dict[str, bool]:
6161
echo "limited_enumeration" || :
6262
[ -f "/usr/bin/vicc" ] && echo "virtualsmartcard" || :
6363
[ -f "/usr/bin/umockdev-run" ] && echo "umockdev" || :
64+
[ -f "/opt/test_venv/bin/vfido.py" ] && echo "vfido" ||:
6465
""",
6566
log_level=ProcessLogLevel.Error,
6667
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from __future__ import annotations
2+
3+
test_venv = "/opt/test_venv"
4+
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: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
to_list_of_strings,
2626
to_list_without_none,
2727
)
28+
from ..misc.globals import test_venv_bin
29+
from ..roles.client import Client
2830
from ..utils.sssctl import SSSCTLUtils
2931
from ..utils.sssd import SSSDUtils
3032
from .base import BaseLinuxRole, BaseObject
@@ -1026,7 +1028,15 @@ def passkey_add(self, passkey_mapping: str) -> IPAUser:
10261028
self._exec("add-passkey", [passkey_mapping])
10271029
return self
10281030

1029-
def passkey_add_register(
1031+
def passkey_add_register(self, **kwargs) -> str:
1032+
"""wrapper for *_passkey_add_register methods"""
1033+
if "virt_type" in kwargs and kwargs["virt_type"] == "vfido":
1034+
del kwargs["virt_type"]
1035+
return self.vfido_passkey_add_register(**kwargs)
1036+
else:
1037+
return self.umockdev_passkey_add_register(**kwargs)
1038+
1039+
def umockdev_passkey_add_register(
10301040
self,
10311041
*,
10321042
pin: str | int | None,
@@ -1103,6 +1113,64 @@ def passkey_remove(self, passkey_mapping: str) -> IPAUser:
11031113
self._exec("remove-passkey", [passkey_mapping])
11041114
return self
11051115

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

sssd_test_framework/utils/authentication.py

Lines changed: 209 additions & 4 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__ = [
@@ -412,8 +413,28 @@ def password_expired(self, username: str, password: str, new_password: str) -> b
412413

413414
def passkey_with_output(
414415
self,
415-
username: str,
416+
**kwargs,
417+
) -> tuple[int, int, str, str]:
418+
"""wrapper for *_passkey_with_output methods"""
419+
if "virt_type" in kwargs and kwargs["virt_type"] == "vfido":
420+
return self.vfido_passkey_with_output(**kwargs)
421+
else:
422+
return self.umockdev_passkey_with_output(**kwargs)
423+
424+
def passkey(
425+
self,
426+
**kwargs,
427+
) -> bool:
428+
"""wrapper for *_passkey methods"""
429+
if "virt_type" in kwargs and kwargs["virt_type"] == "vfido":
430+
return self.vfido_passkey(**kwargs)
431+
else:
432+
return self.umockdev_passkey(**kwargs)
433+
434+
def umockdev_passkey_with_output(
435+
self,
416436
*,
437+
username: str,
417438
device: str,
418439
ioctl: str,
419440
script: str,
@@ -604,10 +625,10 @@ def passkey_with_output(
604625

605626
return result.rc, cmdrc, stdout, result.stderr
606627

607-
def passkey(
628+
def umockdev_passkey(
608629
self,
609-
username: str,
610630
*,
631+
username: str,
611632
device: str,
612633
ioctl: str,
613634
script: str,
@@ -634,11 +655,195 @@ def passkey(
634655
:return: True if authentication was successful, False otherwise.
635656
:rtype: bool
636657
"""
637-
rc, _, _, _ = self.passkey_with_output(
658+
rc, _, _, _ = self.umockdev_passkey_with_output(
638659
username=username, pin=pin, device=device, ioctl=ioctl, script=script, command=command
639660
)
640661
return rc == 0
641662

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

643848
class SSHAuthenticationUtils(MultihostUtility[MultihostHost]):
644849
"""

sssd_test_framework/utils/gdm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def done(self) -> bool:
125125

126126
result = self.host.conn.exec(["/opt/test_venv/bin/scauto", "gui", "done"])
127127
self.host.conn.exec(["systemctl", "stop", "gdm"], raise_on_error=False)
128+
self.init_completed = False
128129
return result.rc == 0
129130

130131
def login_idp(self, client: Client, username: str, password: str) -> bool:

0 commit comments

Comments
 (0)