mirror of
https://chromium.googlesource.com/chromium/tools/depot_tools.git
synced 2026-01-11 02:31:29 +00:00
auth: add pinentry support fido2 plugin
This CL adds pinentry support to ask for security key PIN. Security key PINs may be required (by the manufacturer, or configured by the user) to perform FIDO2 assertions. PIN entry is done by calling pinentry command (or overridden by LUCI_AUTH_PINENTRY environment variable), which we'll ask users to install during onboarding. Bug: 448235795 Change-Id: Ie87389330668dc5eaf8214699defec094757ca9e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/7004844 Reviewed-by: Jiewei Qian <qjw@chromium.org> Commit-Queue: Jiewei Qian <qjw@chromium.org> Reviewed-by: Chenlin Fan <fancl@chromium.org>
This commit is contained in:
@@ -29,10 +29,13 @@ from contextlib import contextmanager
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import signal
|
import signal
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from typing import BinaryIO
|
import traceback
|
||||||
|
from typing import BinaryIO, Optional
|
||||||
|
|
||||||
from fido2.client import DefaultClientDataCollector
|
from fido2.client import DefaultClientDataCollector
|
||||||
from fido2.client import Fido2Client, UserInteraction, WebAuthnClient
|
from fido2.client import Fido2Client, UserInteraction, WebAuthnClient
|
||||||
@@ -52,7 +55,7 @@ _PLUGIN_HEADER_SIZE = 4
|
|||||||
_EXIT_NO_FIDO2_DEVICES = 11
|
_EXIT_NO_FIDO2_DEVICES = 11
|
||||||
_EXIT_ALL_ASSERTIONS_FAILED = 12
|
_EXIT_ALL_ASSERTIONS_FAILED = 12
|
||||||
_EXIT_NO_MATCHING_CRED = 13
|
_EXIT_NO_MATCHING_CRED = 13
|
||||||
|
_EXIT_PINENTRY_FAILED = 14
|
||||||
|
|
||||||
def read_full(r: BinaryIO, size: int) -> bytes:
|
def read_full(r: BinaryIO, size: int) -> bytes:
|
||||||
"""Read an exact amount of data.
|
"""Read an exact amount of data.
|
||||||
@@ -152,7 +155,72 @@ def encode_plugin_response(a: AuthenticationResponse) -> bytes:
|
|||||||
}).encode('utf-8')
|
}).encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
class DiscardInteraction(UserInteraction):
|
def request_pin_pinentry() -> Optional[str]:
|
||||||
|
"""Requests a PIN entry with pinentry program."""
|
||||||
|
try:
|
||||||
|
# Using pinentry to ask for PIN.
|
||||||
|
# https://www.gnupg.org/documentation/manuals/assuan/Client-requests.html
|
||||||
|
pinentry_path = os.environ.get('LUCI_AUTH_PINENTRY', 'pinentry')
|
||||||
|
proc = subprocess.Popen([pinentry_path],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
traceback.print_exception(e, file=sys.stderr)
|
||||||
|
logging.error("PIN requested, but can't find a pinentry program.")
|
||||||
|
logging.error(
|
||||||
|
"Please install a suitable pinentry program for your operating system."
|
||||||
|
)
|
||||||
|
sys.exit(_EXIT_PINENTRY_FAILED)
|
||||||
|
|
||||||
|
pinentry_input = """
|
||||||
|
OPTION ttyname=/dev/tty
|
||||||
|
SETTITLE Chromium Infra Auth
|
||||||
|
SETDESC Enter the FIDO2 PIN for your security key, then touch your security key to continue.
|
||||||
|
SETPROMPT PIN:
|
||||||
|
GETPIN
|
||||||
|
"""
|
||||||
|
stdout, stderr = proc.communicate(pinentry_input)
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
logging.error('pinentry failed: %s', stderr)
|
||||||
|
sys.exit(_EXIT_PINENTRY_FAILED)
|
||||||
|
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
if line.startswith('D '):
|
||||||
|
return line[2:].strip()
|
||||||
|
|
||||||
|
logging.warning(
|
||||||
|
'An empty PIN was entered. Security key assertion may fail.')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def request_pin_mac() -> Optional[str]:
|
||||||
|
"""Request a PIN entry with macOS's built-in `osascript` utility."""
|
||||||
|
osascript_command = (
|
||||||
|
'text returned of ('
|
||||||
|
'display'
|
||||||
|
' dialog "Enter security key PIN.\\n\\nThen touch your security key to continue."'
|
||||||
|
' default answer ""'
|
||||||
|
' with hidden answer'
|
||||||
|
' with title "Chromium Infra Auth"'
|
||||||
|
')')
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
['osascript', '-e', osascript_command],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logging.error("PIN entry failed: %s", result.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class PinEntryInteraction(UserInteraction):
|
||||||
"""Handler when user interaction is required.
|
"""Handler when user interaction is required.
|
||||||
|
|
||||||
This plugin's stdin/stdout talks with git-credential-luci, so we fail
|
This plugin's stdin/stdout talks with git-credential-luci, so we fail
|
||||||
@@ -164,14 +232,18 @@ class DiscardInteraction(UserInteraction):
|
|||||||
sys.stderr.write("\nTouch your blinking security key to continue.\n\n")
|
sys.stderr.write("\nTouch your blinking security key to continue.\n\n")
|
||||||
|
|
||||||
def request_pin(self, permissions, rp_id):
|
def request_pin(self, permissions, rp_id):
|
||||||
# This plugin shouldn't set assertion flags that will require
|
"""Ask for PIN entry with a GUI dialog by using a system tool.
|
||||||
# PIN entry.
|
|
||||||
return None
|
We only handle Linux and MacOS here. We use Windows WebAuthn API
|
||||||
|
directly, which handles PIN entry if necessary.
|
||||||
|
"""
|
||||||
|
if sys.platform == 'darwin':
|
||||||
|
return request_pin_mac()
|
||||||
|
return request_pin_pinentry()
|
||||||
|
|
||||||
def request_uv(self, permissions, rp_id):
|
def request_uv(self, permissions, rp_id):
|
||||||
# Don't allow user verification (UV), because we don't allow PIN
|
# Allows PIN entry.
|
||||||
# entry, UV will fail.
|
return True
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_clients(origin: str) -> list[tuple[WebAuthnClient, str]]:
|
def get_clients(origin: str) -> list[tuple[WebAuthnClient, str]]:
|
||||||
@@ -190,7 +262,7 @@ def get_clients(origin: str) -> list[tuple[WebAuthnClient, str]]:
|
|||||||
logging.debug("Using WindowsClient")
|
logging.debug("Using WindowsClient")
|
||||||
return [(WindowsClient(client_data_collector), "WindowsWebAuthn")]
|
return [(WindowsClient(client_data_collector), "WindowsWebAuthn")]
|
||||||
|
|
||||||
user_interaction = DiscardInteraction()
|
user_interaction = PinEntryInteraction()
|
||||||
clients = []
|
clients = []
|
||||||
for dev in CtapHidDevice.list_devices():
|
for dev in CtapHidDevice.list_devices():
|
||||||
desc = dev.descriptor
|
desc = dev.descriptor
|
||||||
|
|||||||
Reference in New Issue
Block a user