Skip to content

Instantly share code, notes, and snippets.

@sorz
Created January 27, 2025 10:16
Show Gist options
  • Save sorz/ca592a34dd2f01ba8cc782a49d68171b to your computer and use it in GitHub Desktop.
Save sorz/ca592a34dd2f01ba8cc782a49d68171b to your computer and use it in GitHub Desktop.
Enable your FIDO SSH key (ed25519-sk) ONLY IF the hardware key (e.g. YubiKey) is plugged in. (Auto disable key when plug it out). For Windows / OpenSSH.

Having multiple YubiKeys (+ fallback auth method) for ssh is annoying: I must keep pressing ESC until the right key kicks in.

This script is intended to be executed every time your hardware key is plugged in or out. It checks current inserted hardware keys and comment/uncomment IdentityFile /path/to/id_<ID> lines in ~/.ssh.config.

You may follow this post to setup a Scheduled Task (two actually, one for plug, one for unplug) to invoke this script.

#!/usr/bin/env python3
import sys
import re
from os.path import expanduser
from wmi import WMI # pip install wmi
SSH_CONFIG = '~/.ssh/config'
# HardwareID to SSH ID (e.g. yubikey for ~/.ssh/id_yubikey)
# Get Hardware ID with:
# wmic path Win32_PnPEntity where '(PNPClass="SmartCard")' get Name,HardwareID
DEVICE_MAP = {
'SCFILTER\\CID_123456789': 'yubikey',
'SCFILTER\\CID_987654321': 'canokey',
}
def get_current_devices() -> set[str]:
wmi = WMI()
devs = wmi.Win32_PnPEntity(PNPClass='SmartCard')
ids = (d.HardwareID[0] for d in devs if d.HardwareID)
return set(DEVICE_MAP[id] for id in ids if id in DEVICE_MAP)
def update_config(devs: dict[str, bool]):
ssh_config = expanduser(SSH_CONFIG)
with open(ssh_config, encoding='utf-8') as f:
conf = f.read()
lines = conf.split('\n')
updated = False
for i, line in enumerate(lines):
m = re.match(r'^\s+(#?)IdentityFile\s+.*?id_(\w+)', line)
if not m:
continue
enabled = m.group(1) != '#'
id = m.group(2)
if id not in devs:
continue
if enabled ^ devs[id]:
lines[i] = f"{line[:m.start(1)]}{'' if devs[id] else '#'}{line[m.end(1):]}"
updated = True
if not updated:
print('File is uptodate')
return
output = '\n'.join(lines)
with open(ssh_config, 'w', encoding='utf-8') as f:
f.write(output)
print('File updated')
def main():
devs = get_current_devices()
if len(sys.argv) > 1:
action = sys.argv[1]
else:
action = None
if action == 'plug-in' and not devs:
print('No device, do nothing')
return
update_config({d: d in devs for d in DEVICE_MAP.values()})
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment