-
Notifications
You must be signed in to change notification settings - Fork 27
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
Nitrokey Start ID switching without sudo #305
base: master
Are you sure you want to change the base?
Changes from 5 commits
518b7d4
99953d0
9306b89
8818cd5
ae1e375
8767cf3
c1534df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,6 +55,7 @@ semi-clean: | |
clean: semi-clean | ||
rm -rf $(VENV) | ||
rm -rf dist | ||
rm -rf ./.mypy_cache/ | ||
|
||
|
||
# Package management | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,26 +6,22 @@ | |
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or | ||
# http://opensource.org/licenses/MIT>, at your option. This file may not be | ||
# copied, modified, or distributed except according to those terms. | ||
|
||
|
||
from subprocess import check_output | ||
import binascii | ||
import typing | ||
from sys import stderr, stdout | ||
from time import sleep | ||
|
||
import click | ||
from tqdm import tqdm | ||
from usb.core import USBError | ||
|
||
from pynitrokey.confconsts import logger | ||
from pynitrokey.helpers import local_critical, local_print | ||
from pynitrokey.start.gnuk_token import OnlyBusyICCError, get_gnuk_device | ||
from pynitrokey.start.threaded_log import ThreadLog | ||
from pynitrokey.start.upgrade_by_passwd import ( | ||
DEFAULT_PW3, | ||
DEFAULT_WAIT_FOR_REENUMERATION, | ||
IS_LINUX, | ||
kill_smartcard_services, | ||
logger, | ||
restart_smartcard_services, | ||
show_kdf_details, | ||
start_update, | ||
validate_gnuk, | ||
|
@@ -93,14 +89,79 @@ def rng(count, raw, quiet): | |
print(challenge.hex()) | ||
|
||
|
||
class CardRemovedGPGAgentException(RuntimeWarning): | ||
pass | ||
|
||
|
||
class NoCardPCSCException(RuntimeWarning): | ||
pass | ||
|
||
|
||
def gpg_agent_set_identity(identity: int): | ||
from subprocess import check_output as run | ||
|
||
cmd = f"gpg-connect-agent" f"|SCD APDU 00 85 00 0{identity}" f"|/bye".split("|") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need an f-string for all of those strings. |
||
app_out = run(cmd) | ||
logger.debug(cmd) | ||
logger.debug(app_out) | ||
if b"ERR 100663406 Card removed" in app_out: | ||
raise CardRemovedGPGAgentException("Card removed") | ||
|
||
|
||
def pcsc_set_identity(identity): | ||
try: | ||
from smartcard import System | ||
from smartcard.CardConnection import CardConnection | ||
from smartcard.Exceptions import NoCardException | ||
|
||
def find_smartcard(name: typing.Optional[str] = None) -> CardConnection: | ||
# use this for debug: sudo pcscd -f -a | ||
for reader in System.readers(): | ||
if "Nitrokey Start" not in str(reader) or ( | ||
name and name not in str(reader) | ||
): | ||
continue | ||
conn: CardConnection | ||
conn = reader.createConnection() | ||
try: | ||
conn.connect() | ||
except NoCardException: | ||
continue | ||
logger.debug([reader, conn]) | ||
return conn | ||
raise NoCardPCSCException(f"No smartcard with UUID {name} found") | ||
|
||
def select(conn: CardConnection, aid: bytes) -> bool: | ||
apdu = [0x00, 0xA4, 0x04, 0x00] | ||
apdu.append(len(aid)) | ||
apdu.extend(aid) | ||
_, sw1, sw2 = conn.transmit(apdu) | ||
return (sw1, sw2) == (0x90, 0x00) | ||
|
||
def send_id_change(conn: CardConnection, identity: int) -> None: | ||
out = [0x00, 0x85, 0x00, identity] | ||
for i in range(2): | ||
data, sw1, sw2 = conn.transmit(out) | ||
logger.debug((bytes(out).hex(), data, hex(sw1), hex(sw2))) | ||
res = bytes([sw1, sw2]).hex() | ||
if res == "9000": | ||
break | ||
if res == "6a88": | ||
logger.debug(f"error: {res}") | ||
continue | ||
|
||
conn = find_smartcard() | ||
aid = binascii.a2b_hex("D276:0001:2401".replace(":", "")) | ||
select(conn, aid) | ||
send_id_change(conn, identity) | ||
|
||
except ImportError: | ||
logger.debug("pcsc feature is deactivated, skipping this method") | ||
|
||
|
||
@click.command() | ||
@click.argument("identity") | ||
@click.option( | ||
"--force-restart", | ||
is_flag=True, | ||
help="Force pcscd and GnuPG services restart once finished", | ||
) | ||
def set_identity(identity, force_restart): | ||
def set_identity(identity): | ||
"""Set given identity (one of: 0, 1, 2) | ||
|
||
Might require stopping other smart card services to connect directly to the device over CCID interface. | ||
|
@@ -118,7 +179,62 @@ def set_identity(identity, force_restart): | |
local_critical("identity must be 0, 1 or 2") | ||
|
||
local_print(f"Setting identity to {identity}") | ||
for x in range(3): | ||
|
||
# Note: the error in communication coming after changing the identity is caused by the immediate restart of | ||
# the device, without responding to the call. The idea was to avoid operating with a potentially inconsistent state in | ||
# the memory. | ||
def inner() -> None: | ||
""" | ||
Call all the methods in the order of the success chance | ||
""" | ||
|
||
# this works when gpg has opened connection, stops after changing identity with it | ||
try: | ||
logger.info("Trying changing identity through gpg-agent") | ||
gpg_agent_set_identity(identity) | ||
return | ||
except CardRemovedGPGAgentException: | ||
# this error shows up when the identity was just changed with gpg, and the new state was not reloaded | ||
pass | ||
|
||
try: | ||
from smartcard.Exceptions import CardConnectionException # type: ignore | ||
from smartcard.pcsc.PCSCExceptions import EstablishContextException | ||
|
||
# this works when gpg has no connection, but pcsc server is working | ||
try: | ||
logger.info("Trying changing identity through pcscd") | ||
pcsc_set_identity(identity) | ||
logger.info( | ||
f"Device returns success. Identity is already set to {identity}." | ||
) | ||
return | ||
except CardConnectionException as e: | ||
logger.debug(f"Expected error. PCSC reports {e}") | ||
return | ||
except (EstablishContextException, NoCardPCSCException) as e: | ||
logger.debug( | ||
f"pcscd must not work, try another method. Reported error: {e}" | ||
) | ||
except ImportError: | ||
local_print("pcsc feature is deactivated, skipping this switching method") | ||
|
||
# this works, when neither gnupg nor opensc is claiming the smart card interface | ||
try: | ||
logger.info("Trying changing identity through direct connection") | ||
set_identity_raw(identity) | ||
except: | ||
raise | ||
|
||
inner() | ||
# apparently calling it 2 times reloads the gnupg, and allows for immediate use of it after changing the identity | ||
# otherwise its reload is needed with gpgconf --reload all | ||
inner() | ||
local_print(f"Reset done - now active identity: {identity}") | ||
|
||
|
||
def set_identity_raw(identity): | ||
for x in range(1): | ||
try: | ||
gnuk = get_gnuk_device() | ||
with gnuk.release_on_exit() as gnuk: | ||
|
@@ -127,7 +243,7 @@ def set_identity(identity, force_restart): | |
gnuk.cmd_set_identity(identity) | ||
break | ||
except USBError: | ||
local_print(f"Reset done - now active identity: {identity}") | ||
logger.debug(f"Reset done - now active identity: {identity}") | ||
break | ||
|
||
except OnlyBusyICCError: | ||
|
@@ -137,8 +253,8 @@ def set_identity(identity, force_restart): | |
break | ||
except ValueError as e: | ||
if "No ICC present" in str(e): | ||
local_print( | ||
"Device is occupied by some other service and cannot be connected to. Identity not changed." | ||
local_critical( | ||
"Device is not connected or occupied by some other service and cannot be connected to. Identity not changed." | ||
) | ||
else: | ||
local_critical(e) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,8 @@ class Verbosity(IntEnum): | |
f"environment variable: '{ENV_DEBUG_VAR}' invalid, " | ||
f"setting default: {VERBOSE.name} = {VERBOSE.value}" | ||
) | ||
print(f"Found {ENV_DEBUG_VAR}='{_env_dbg_lvl}'. Setting VERBOSE={VERBOSE}") | ||
|
||
|
||
LOG_FN = tempfile.NamedTemporaryFile(prefix="nitropy.log.").name | ||
LOG_FORMAT_STDOUT = "%(asctime)-15s %(levelname)6s %(name)10s %(message)s" | ||
|
@@ -47,3 +49,10 @@ class Verbosity(IntEnum): | |
UDEV_URL = ( | ||
"https://docs.nitrokey.com/nitrokey3/linux/firmware-update.html#troubleshooting" | ||
) | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
stream_handler = logging.StreamHandler() | ||
stream_handler.setLevel(VERBOSE) | ||
stream_handler.setFormatter(logging.Formatter(LOG_FORMAT_STDOUT)) | ||
logger.addHandler(stream_handler) | ||
Comment on lines
+54
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default verbosity level should probably be higher than INFO to avoid printing to console. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,5 +97,6 @@ module = [ | |
"usb1.*", | ||
"tlv8.*", | ||
"pytest.*", | ||
"smartcard.pcsc.*", | ||
] | ||
ignore_missing_imports = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d prefer removing that rename because there also is
subprocess.run
.