Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YubiKey PIN input #48

Closed
wants to merge 45 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
36a25f6
build ui
basvandriel Dec 5, 2024
a834b3a
disable text field
basvandriel Dec 5, 2024
9022e1b
Toggle text field
basvandriel Dec 5, 2024
b33b26a
Work on passing yubikey details
basvandriel Dec 6, 2024
1fa1755
Cleanup and select yubikey
basvandriel Dec 6, 2024
9594fe3
add lib
basvandriel Dec 6, 2024
dfaa9aa
work on notify
basvandriel Dec 6, 2024
2e0b501
cleanup
basvandriel Dec 6, 2024
24f4456
Give signal to commit if pin was succesfull
basvandriel Dec 6, 2024
128e509
Cleanup
basvandriel Dec 6, 2024
e9ca798
clenaup
basvandriel Dec 6, 2024
3d8b817
work on yubikey input
basvandriel Dec 6, 2024
057d7b7
Refactor auth logic into seperate class
basvandriel Dec 6, 2024
a477f9b
Fix auth button
basvandriel Dec 6, 2024
a129556
add todo
basvandriel Dec 9, 2024
42cabce
work
basvandriel Dec 9, 2024
d7d524b
add todo
basvandriel Dec 9, 2024
db72947
work
basvandriel Dec 9, 2024
10ce9a1
work
basvandriel Dec 9, 2024
2dffbff
Cleanup pin authenticator
basvandriel Dec 9, 2024
5505025
work
basvandriel Dec 9, 2024
f22281d
cleanup
basvandriel Dec 9, 2024
1909b5a
cleanup page
basvandriel Dec 9, 2024
444c52b
work
basvandriel Dec 9, 2024
72b7d93
cleanup
basvandriel Dec 9, 2024
22a2f53
work
basvandriel Dec 9, 2024
ce2ad24
Cleanup tests
basvandriel Dec 9, 2024
38b0914
work
basvandriel Dec 10, 2024
806eab5
add bad pin case
basvandriel Dec 10, 2024
396c74a
test yubipin
basvandriel Dec 10, 2024
fff5b6b
add test for page
basvandriel Dec 10, 2024
5f3b0e6
Check
basvandriel Dec 10, 2024
f843c2a
add test for selecting
basvandriel Dec 10, 2024
71dce4e
cleanup
basvandriel Dec 10, 2024
c18c2c8
Add deselect and cleanup test
basvandriel Dec 10, 2024
3563e6b
set pin
basvandriel Dec 10, 2024
bce25f9
work
basvandriel Dec 10, 2024
5f51e89
cleanup
basvandriel Dec 10, 2024
48bcd15
provide pin
basvandriel Dec 10, 2024
703bb2a
Remove comment
basvandriel Dec 10, 2024
bc88848
Remove yubipin in .env
basvandriel Dec 10, 2024
4d959ad
Cleanup
basvandriel Dec 10, 2024
1024cdb
cleanup
basvandriel Dec 10, 2024
fb9f92a
fix docs
basvandriel Dec 10, 2024
6c88b57
Merge branch 'uzipoc_q4_2024' into feature/yubipin
basvandriel Dec 10, 2024
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
7 changes: 6 additions & 1 deletion app/page/requestcert.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ def __init__(self, mypkcs, myacme, parent=None):
def initializePage(self):
QTimer.singleShot(500, self.natimer)

def _get_yubikey_pin(self) -> str:
return self.wizard().property("yubikey_pin")

def natimer(self):
self.setTitle("Request Certificate")
layout = QVBoxLayout(self)
label = QLabel("Certificate Requests")
layout.addWidget(label)
selectedYubiKeySlot, _, _ = self.wizard().property("selectedYubiKey")
yubikey_pin = self._get_yubikey_pin()

jwttoken = self.wizard().property("jwt_token")()
f9crt = self.pkcs.getf9(selectedYubiKeySlot)
for keynum in [4, 3, 2, 1]:
hwattest = self.pkcs.getattest(selectedYubiKeySlot, keynum)
self.acme.send_request(hwattest, jwttoken, keynum - 1, f9crt)
self.acme.wait(keynum - 1)
csr = self.pkcs.getcsr(selectedYubiKeySlot, keynum)
csr = self.pkcs.getcsr(selectedYubiKeySlot, keynum, yubikey_pin)
cert = self.acme.final(keynum, csr, jwttoken)
self.pkcs.savecert(selectedYubiKeySlot, keynum, cert)
self.wizard().next() # Programmatically trigger the Next button
78 changes: 65 additions & 13 deletions app/page/selectkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,25 @@
QListWidgetItem,
)

from app.page.yubipin_widget import YubiPinWidget
from app.yubikey_details import YubikeyDetails

from .yubikeyitem import YubiKeyItemWidget
from app.pkcs import pkcs as InternalPKCSWrapper


class SelectYubiKeyPage(QWizardPage):
_TITLE = "Selectie en authenticatie Yubikey"
_SUBTITLE = """
Selecteer de desbetreffende Yubikey en vul vervolgens hiervan de PIN-code in. De standaard PIN-code is alvast ingevuld.
"""

pkcs: InternalPKCSWrapper
key_list_widget: QListWidget
_pin_input_widget: YubiPinWidget

# This will be set when the user has succesfully authenticated
_pin_authenticated: bool

def _prevent_backbutton_clicks(self):
self.setCommitPage(True)
Expand All @@ -28,6 +42,8 @@ def _get_yubikeys(self):

def _build_yubikey_list_widget(self, yubikeys: list[Any]) -> QListWidget:
widget = QListWidget()
widget.itemSelectionChanged.connect(self.on_yubikey_item_change)
widget.setSelectionMode(QListWidget.SelectionMode.SingleSelection)

for name, serial, available, slot in yubikeys:
itemWidget = YubiKeyItemWidget(name, serial, available, slot)
Expand All @@ -39,19 +55,31 @@ def _build_yubikey_list_widget(self, yubikeys: list[Any]) -> QListWidget:

return widget

def __init__(self, mypkcs, parent=None):
super().__init__(parent)
self.setTitle("Selecteer de te gebruiken yubikey")
def _setup_ui(self, pkcslib: InternalPKCSWrapper):
self.setTitle(self._TITLE)
self.setSubTitle(self._SUBTITLE.strip())

self._prevent_backbutton_clicks()

self.pkcs = mypkcs
yubikeys = self._get_yubikeys()
yubikey_list_widget = self._build_yubikey_list_widget(yubikeys)

layout = QVBoxLayout(self)
layout.addWidget(yubikey_list_widget)

yubipin_widget = YubiPinWidget(pkcslib.pkcs11)
yubipin_widget.pin_authenticated_signal.connect(self._on_yubikey_authentication)
layout.addWidget(yubipin_widget)

self.key_list_widget = yubikey_list_widget
self._pin_input_widget = yubipin_widget

def __init__(self, mypkcs: InternalPKCSWrapper, parent=None):
super().__init__(parent)
self.pkcs = mypkcs
self._pin_authenticated = False

self._setup_ui(mypkcs)

def _find_selected_widget_item(self) -> Optional[YubiKeyItemWidget]:
selected_indexes = self.key_list_widget.selectedIndexes()
Expand All @@ -68,33 +96,57 @@ def _find_selected_widget_item(self) -> Optional[YubiKeyItemWidget]:

return widget

def _find_selected_yubikey_details_from_widget(self):
selected_yubikey_widget_item: Optional[YubiKeyItemWidget] = self._find_selected_widget_item()

if selected_yubikey_widget_item is None:
return None

slot, name, serial = selected_yubikey_widget_item.getYubiKeyDetails()
return YubikeyDetails(
slot,
name,
serial,
)

def on_yubikey_item_change(self):
# The next button does not have to be enabled manually, just trigger the completion signal.
# This will re-check the isCompleted function
# https://doc.qt.io/qt-6/qwizardpage.html#completeChanged
selected_yubikey: Optional[YubikeyDetails] = self._find_selected_yubikey_details_from_widget()

if not selected_yubikey:
self._pin_authenticated = False

# Pass none if nothing is selected
self._pin_input_widget.toggle_input_field_ability(details=selected_yubikey)
self.completeChanged.emit()

def isComplete(self) -> bool:
def has_selection(self) -> bool:
return self._find_selected_widget_item() is not None

def isComplete(self) -> bool:
return self.has_selection() and self._pin_authenticated

def nextId(self):
widget = self._find_selected_widget_item()

# This should and can not happen, since we're disabling the button
if not widget:
if not widget or not self.isComplete():
return super().nextId()

# Store the selected YubiKey information in the wizard
self.wizard().setProperty(
"selectedYubiKey",
widget.getYubiKeyDetails(),
)
return super().nextId()
pin = self._pin_input_widget.get_value()
self.wizard().setProperty("yubikey_pin", pin)

def initializePage(self) -> None:
super().initializePage()
return super().nextId()

self.key_list_widget.itemSelectionChanged.connect(self.on_yubikey_item_change)
def _on_yubikey_authentication(self, authenticated: bool):
self._pin_authenticated = authenticated
self.completeChanged.emit()

def cleanupPage(self) -> None:
self._pin_authenticated = False
self._pin_input_widget.cleanup()
self.key_list_widget.clearSelection()
150 changes: 150 additions & 0 deletions app/page/yubipin_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
from typing import Optional
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtWidgets import QLineEdit, QLabel, QHBoxLayout, QWidget, QPushButton

from app.yubikey_details import YubikeyDetails

from PyKCS11 import PyKCS11Lib

from app.yubikey_pin_authenticator import YubikeyPINAuthenticator


class YubiPinWidget(QWidget):
_DEFAULT_YUBIKEY_PIN = "123456"

_pin_label = QLabel
_input: QLineEdit
_authenticate_button: QPushButton
_notification_text: QLabel

# We can't enforce this into a Optional[YubikeyDetails], so we have to do it like this
_selectedYubiKeySignal = pyqtSignal(object)

# This is being called when the page above cleans up
_cleanup_signal = pyqtSignal()

# This will get updated based on the incoming signal
_selected_yubikey: Optional[YubikeyDetails]

# The lib used for authenticating
_pykcs_lib: PyKCS11Lib

# This signal is conected from outside
pin_authenticated_signal = pyqtSignal(bool)

def _build_label(self):
label = QLabel("PIN")
label.setEnabled(False)

return label

def _build_input(self):
i = QLineEdit(self._DEFAULT_YUBIKEY_PIN)

# By default, the button is enabled, but should be enabled when a YubiKey is selected
i.setEnabled(False)
i.setEchoMode(QLineEdit.EchoMode.Password)

i.textChanged.connect(self._on_pin_edit)

return i

def _build_auth_button(self):
button = QPushButton("Authenticate")
button.setEnabled(False)
button.clicked.connect(self._authenticate)

return button

def _setup_ui(self):
layout = QHBoxLayout()

label = self._build_label()
layout.addWidget(label)
self._pin_label = label

pin_input = self._build_input()
layout.addWidget(pin_input)

button = self._build_auth_button()
layout.addWidget(button)

notification_text = QLabel()
notification_text.hide()
layout.addWidget(notification_text)
self._notification_text = notification_text

# Set this layout as the layout of the widget
self.setLayout(layout)

self._selectedYubiKeySignal.connect(self._internal_select_yubikey)
self._cleanup_signal.connect(self._on_cleanup_signal)

self._input = pin_input
self._authenticate_button = button

def __init__(self, pykcs11lib: PyKCS11Lib) -> None:
super().__init__(None)
self._setup_ui()
self._pykcs_lib = pykcs11lib
self._selected_yubikey = None

def get_value(self) -> str:
return self._input.text()

def _notify_pin_ok(self):
self._notification_text.setText("OK")
self._notification_text.show()
self.pin_authenticated_signal.emit(True)

def _notify_pin_incorrect(self):
self._notification_text.setText("PIN incorrect")
self._notification_text.show()

self.pin_authenticated_signal.emit(False)

def _authenticate(self):
if not self._selected_yubikey:
return
ok: bool = YubikeyPINAuthenticator(self._pykcs_lib).login(
self._selected_yubikey,
self.get_value(),
)

if not ok:
self._notify_pin_incorrect()
return

self._notify_pin_ok()

# Close all sessions so we can log back in on later moments
self._pykcs_lib.closeAllSessions(self._selected_yubikey.slot)

def _on_pin_edit(self, value: str):
empty_text: bool = bool(not value.strip())
disable_auth_button: bool = empty_text and self._authenticate_button.isEnabled()

self._authenticate_button.setEnabled(not disable_auth_button)

def toggle_input_field_ability(self, details: Optional[YubikeyDetails]):
self._selectedYubiKeySignal.emit(details)

def cleanup(self):
self._cleanup_signal.emit()

def _on_cleanup_signal(self):
self._notification_text.hide()

def _internal_select_yubikey(self, details: Optional[YubikeyDetails]):
on = details is not None

# Update the selected yubikey if any
self._selected_yubikey = details

self._pin_label.setEnabled(on)
self._input.setEnabled(on)

# We don't want to always enable the auth button. Text has to be in there too.
should_enable_auth_button: bool = on and self.get_value() != ""

self._authenticate_button.setEnabled(should_enable_auth_button)
23 changes: 12 additions & 11 deletions app/pkcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class AlgorithmIdentifier(Sequence):


class pkcs:
_3DES_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708"

DEFAULT_LIB_LOCATION = "/usr/lib64/libykcs11.so.2"
DEFAULT_HOMEBREW_LOCATION = "/opt/homebrew/lib/libykcs11.dylib"

Expand All @@ -29,27 +31,26 @@ class pkcs:
keys = {1: 1, 2: 2, 3: 3, 4: 4}

pkcs11: PyKCS11.PyKCS11Lib
_yubikey_pin: str

def __init__(self, pykcs11lib: PyKCS11.PyKCS11Lib, yubikey_pin: str):
def __init__(self, pykcs11lib: PyKCS11.PyKCS11Lib):
self.pkcs11 = pykcs11lib
self._yubikey_pin = yubikey_pin

def getusersession(self, slot):
def getusersession(self, slot, yubikey_pin: str):
print("User Open", slot)
if slot not in self.sessions:
self.sessions[slot] = self.pkcs11.openSession(slot)
self.sessions[slot].login(self._yubikey_pin)
self.sessions[slot].login(yubikey_pin)
return self.sessions[slot]

def getsession(self, slot):
print("Open", slot)

if slot not in self.sessions:
# self.sessions[slot] = self.pkcs11.openSession(slot)
# self.sessions[slot].login("123456")
self.sessions[slot] = self.pkcs11.openSession(slot, PyKCS11.CKF_RW_SESSION)

# This works because it's logging in with the 3DES management key
self.sessions[slot].login(
"010203040506070801020304050607080102030405060708",
self._3DES_MANAGEMENT_KEY,
user_type=PyKCS11.CKU_SO,
)
return self.sessions[slot]
Expand All @@ -59,7 +60,7 @@ def getadminsession(self, slot):
if slot not in self.sessions:
self.sessions[slot] = self.pkcs11.openSession(slot, PyKCS11.CKF_RW_SESSION)
self.sessions[slot].login(
"010203040506070801020304050607080102030405060708",
self._3DES_MANAGEMENT_KEY,
user_type=PyKCS11.CKU_SO,
)
return self.sessions[slot]
Expand Down Expand Up @@ -205,9 +206,9 @@ def makecsr(self, session, private_key, public_key):
)
return csr.dump()

def getcsr(self, slot, keyid):
def getcsr(self, slot, keyid, yubikey_pin: str):
csr = None
session = self.getusersession(slot)
session = self.getusersession(slot, yubikey_pin)
privkey = session.findObjects([(PyKCS11.CKA_CLASS, PyKCS11.CKO_PRIVATE_KEY), (PyKCS11.CKA_ID, [keyid])])
pubkey = session.findObjects([(PyKCS11.CKA_CLASS, PyKCS11.CKO_PUBLIC_KEY), (PyKCS11.CKA_ID, [keyid])])
if privkey and pubkey:
Expand Down
5 changes: 1 addition & 4 deletions app/wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,9 @@ def __init__(self, mypkcs, myacme, oidc_provider_base_url: urllib.parse.ParseRes

app = QApplication(sys.argv)

yubikey_pin = getenv(
"YUBIKEY_PIN",
)
# This will search default locations and fall back to the PYKCS11LIB environment variable
pkcslib = PKCS11LibFinder().find()
pkcscls = pkcs(pykcs11lib=pkcslib, yubikey_pin=yubikey_pin)
pkcscls = pkcs(pykcs11lib=pkcslib)

oidc_provider_url = urllib.parse.urlparse(getenv("OIDC_PROVIDER_BASE_URL", DEFAULT_PROEFTUIN_OIDC_LOGIN_URL))
print(
Expand Down
Loading
Loading