Skip to content

Commit 698d94f

Browse files
committed
Enhance pattern handling for more flexibility
* Transform PATTERN_KEYS into a list to avoid an unmanageable single complex regex, instead split in several simple regex * Transform hard-coded public and private key determination into configurable pattern lists: PATTERN_PUBKEYS, PATTERN_PRIVKEYS * A second parameter after the regex can be used to define what part to remove for key pair matching * PATTERN_KEYS, PATTERN_PUBKEYS, PATTERN_PRIVKEYS new only search/test against filename, and nomore their full path * Compile regex for PATTERN_KEYS, PATTERN_PUBKEYS, PATTERN_PRIVKEYS, MATCH_PATH, MATCH_ARGV for better performance. This is done in a general way within Config.get() * Add some debug prints for analyzing pattern matching * Add debug print of found key pairs including hint if corresponding public or private key couldn't be found * Enhance docstring to reflect changes * Enhance docstring with more details to key pair handling and pattern matching * Enhance docstring with already present features of per-identity ssh config file
1 parent 746d174 commit 698d94f

File tree

1 file changed

+183
-46
lines changed

1 file changed

+183
-46
lines changed

ssh-ident

Lines changed: 183 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ In any case, ssh-ident:
2222
actually need them, once. No matter how many terminals, ssh or login
2323
sessions you have, no matter if your home is shared via NFS.
2424
25-
- can prepare and use a different agent and different set of keys depending
26-
on the host you are connecting to, or the directory you are using ssh
27-
from.
25+
- can prepare and use a different agent, different set of keys and different
26+
ssh config file depending on the host you are connecting to, or the
27+
directory you are using ssh from.
2828
This allows for isolating keys when using agent forwarding with different
2929
sites (eg, university, work, home, secret evil internet identity, ...).
3030
It also allows to use multiple accounts on sites like github, unfuddle
@@ -199,6 +199,7 @@ To have multiple identities, all I have to do is:
199199
# need it.
200200
# Otherwise, provides options to be passed to 'ssh' for specific
201201
# identities.
202+
# Note that separate ssh config files per identity are possible too.
202203
SSH_OPTIONS = {
203204
# Disable forwarding of the agent, but enable X forwarding,
204205
# when using the work profile.
@@ -241,6 +242,11 @@ To have multiple identities, all I have to do is:
241242
# Generate keys to be used for work only, rsa
242243
$ ssh-keygen -t rsa -b 4096 -f ~/.ssh/identities/work/id_rsa
243244
245+
5) Optionally create separate ssh config files for those identities that
246+
need special ssh settings in general or for specific hosts:
247+
248+
$ ${EDITOR} ~/.ssh/identities/secret/config
249+
244250
...
245251
246252
@@ -260,18 +266,79 @@ ssh-ident will be invoked instead, and:
260266
access only to the agent for the identity work, and the corresponding
261267
keys.
262268
263-
Note that ssh-ident needs to access both your private and public keys. Note
264-
also that it identifies public keys by the .pub extension. All files in your
265-
identities subdirectories will be considered keys.
266-
267-
If you want to only load keys that have "key" in the name, you can add
268-
to your .ssh-ident:
269-
270-
PATTERN_KEYS = "key"
269+
Notes about key files:
270+
ssh-ident needs to access both your private and public keys. Both files of each
271+
key pair have to reside in the same directory.
272+
All files in your identities subdirectories that match PATTERN_KEYS will be
273+
considered key files (either private or public). If a different naming scheme
274+
is used, then make sure that PATTERN_KEYS matches filenames for both types.
275+
By default ssh-ident identifies public keys by the .pub extension or
276+
a "public" inside the filename, while private keys have no explicit extension
277+
or a "private" inside the filename. To recognize a key pair these specific name
278+
parts are removed, the remaining filenames compared and connected if they match.
279+
A key is only recognized and loaded if the key pair is complete.
280+
The public key file is necessary to detect if a key is already loaded into
281+
ssh-agent to avoid adding it again and therefore asking for password again.
282+
If a public key file is missing check out the '-y' parameter of 'ssh-keygen'.
283+
All patterns to detect key files in general plus public and private keys are
284+
defined in lists, which can hold multiple regular expressions or simple
285+
compare strings. The first match is taken and no further tests done.
286+
Patterns are tested against the filename, not the full path.
287+
288+
The defaults of PATTERN_KEYS against the filename for the general key file
289+
determination are:
290+
291+
PATTERN_KEYS = [
292+
r"^id_",
293+
r"^identity",
294+
r"^ssh[0-9]-",
295+
]
296+
297+
The defaults of PATTERN_PUBKEYS and PATTERN_PRIVKEYS for the public and private
298+
key determination are:
299+
300+
PATTERN_PUBKEYS = [
301+
[r"\.pub$", 0],
302+
[r"public", 0],
303+
]
304+
PATTERN_PRIVKEYS = [
305+
[r"private", 0],
306+
# Fallback for all remaining files.
307+
[r"", None],
308+
]
309+
310+
Notes about PATTERN_PUBKEYS and PATTERN_PRIVKEYS:
311+
ssh-ident first checks if the file is a public key, then if it is a private key.
312+
The second parameter after the patterns defines which group to remove to
313+
recognize key pairs. A zero (0) means remove the whole match. 'None' means do
314+
not remove anything and leave filename as is.
315+
316+
If you want to only load keys that have "mykey" in their filename, you can
317+
define in your .ssh-ident:
318+
319+
PATTERN_KEYS = [
320+
"mykey",
321+
]
322+
323+
If you want to also load keys that have the extension ".key" or ".pub", then
324+
you can define in your .ssh-ident:
325+
326+
PATTERN_KEYS = [
327+
r"^id_",
328+
r"^identity",
329+
r"^ssh[0-9]-",
330+
r"(\.key|\.pub)$",
331+
]
332+
PATTERN_PRIVKEYS = [
333+
[r"\.key$", 0],
334+
[r"private", 0],
335+
# Fallback for all remaining files.
336+
[r"", None],
337+
]
338+
339+
Note: As the ".pub" and ".key" patterns come first, those filenames can also
340+
have "public" or "private" in their name, e.g. their user name.
271341
272-
The default is:
273-
274-
PATTERN_KEYS = r"/(id_.*|identity.*|ssh[0-9]-.*)"
275342
276343
You can also redefine:
277344
@@ -435,7 +502,21 @@ class Config(object):
435502
"DIR_AGENTS": "$HOME/.ssh/agents",
436503

437504
# How to identify key files in the identities directory.
438-
"PATTERN_KEYS": r"/(id_.*|identity.*|ssh[0-9]-.*)",
505+
"PATTERN_KEYS": [
506+
r"^id_",
507+
r"^identity",
508+
r"^ssh[0-9]-",
509+
],
510+
# How to recognize public and private key files in the identities directory.
511+
"PATTERN_PUBKEYS": [
512+
[r"\.pub$", 0],
513+
[r"public", 0],
514+
],
515+
"PATTERN_PRIVKEYS": [
516+
[r"private", 0],
517+
# Fallback for all remaining files.
518+
[r"", None],
519+
],
439520

440521
# How to identify ssh config files.
441522
"PATTERN_CONFIG": r"/config$",
@@ -502,23 +583,49 @@ class Config(object):
502583
def Get(self, parameter):
503584
"""Returns the value of a parameter, or causes the script to exit."""
504585
if parameter in os.environ:
505-
return self.Expand(os.environ[parameter])
506-
if parameter in self.values:
507-
return self.Expand(self.values[parameter])
508-
if parameter in self.defaults:
509-
return self.Expand(self.defaults[parameter])
510-
511-
print(
512-
"Parameter '{0}' needs to be defined in "
513-
"config file or defaults".format(parameter), file=sys.stderr,
514-
loglevel=LOG_ERROR)
515-
sys.exit(2)
586+
result = self.Expand(os.environ[parameter])
587+
elif parameter in self.values:
588+
result = self.Expand(self.values[parameter])
589+
elif parameter in self.defaults:
590+
result = self.Expand(self.defaults[parameter])
591+
else:
592+
print(
593+
"Parameter '{0}' needs to be defined in "
594+
"config file or defaults".format(parameter), file=sys.stderr,
595+
loglevel=LOG_ERROR)
596+
sys.exit(2)
597+
598+
# Compile patterns for speed
599+
if parameter in ("PATTERN_KEYS", "PATTERN_PUBKEYS", "PATTERN_PRIVKEYS", "MATCH_PATH", "MATCH_ARGV"):
600+
# Convert old format string, or wrongly used tuple, to list
601+
if not isinstance(result, list):
602+
# Convert tuple to list, as we need it mutable
603+
if isinstance(result, tuple):
604+
result = list(result)
605+
else:
606+
result = [result]
607+
# Compile regex pattern [in first element] of each list entry
608+
for index, entry in enumerate(result):
609+
# Convert tuple to list, as we need it mutable
610+
if isinstance(entry, tuple):
611+
entry = result[index] = list(entry)
612+
# Compile regex
613+
if isinstance(entry, list):
614+
entry[0] = re.compile(entry[0])
615+
else:
616+
entry = result[index] = re.compile(entry)
617+
#
618+
print("{0} #{1}: {2}".format(parameter, index+1, entry),
619+
file=sys.stderr,
620+
loglevel=LOG_DEBUG)
621+
622+
return result
516623

517624
def Set(self, parameter, value):
518625
"""Sets configuration option parameter to value."""
519626
self.values[parameter] = value
520627

521-
def FindIdentityInList(elements, identities, all_elements):
628+
def FindIdentityInList(elements, identities, all_elements, pattern_name):
522629
"""Matches a list of identities to a list of elements.
523630
524631
Args:
@@ -532,18 +639,22 @@ def FindIdentityInList(elements, identities, all_elements):
532639
"""
533640
# Test against each element separately
534641
for element in elements:
642+
index = 0
535643
for regex, identity in identities:
644+
index += 1
536645
if re.search(regex, element):
537-
print("Matching: {0}".format(element),
646+
print("Matching {0} #{1}: {2}".format(pattern_name, index, element),
538647
file=sys.stderr,
539648
loglevel=LOG_DEBUG)
540649
return identity
541650
# Test against all elements in a single string
542651
if all_elements and len(elements) > 1:
543652
element = " ".join(elements)
653+
index = 0
544654
for regex, identity in identities:
655+
index += 1
545656
if re.search(regex, element):
546-
print("Matching: {0}".format(element),
657+
print("Matching {0} #{1}: {2}".format(pattern_name, index, element),
547658
file=sys.stderr,
548659
loglevel=LOG_DEBUG)
549660
return identity
@@ -562,8 +673,8 @@ def FindIdentity(argv, config):
562673
"""
563674
paths = set([os.getcwd(), os.path.abspath(os.getcwd()), os.path.normpath(os.getcwd())])
564675
return (
565-
FindIdentityInList(argv, config.Get("MATCH_ARGV"), True) or
566-
FindIdentityInList(paths, config.Get("MATCH_PATH"), False) or
676+
FindIdentityInList(argv, config.Get("MATCH_ARGV"), True, "MATCH_ARGV") or
677+
FindIdentityInList(paths, config.Get("MATCH_PATH"), False, "MATCH_PATH") or
567678
config.Get("DEFAULT_IDENTITY"))
568679

569680
def FindKeys(identity, config):
@@ -587,7 +698,9 @@ def FindKeys(identity, config):
587698
if identity == getpass.getuser():
588699
directories.append(os.path.expanduser("~/.ssh"))
589700

590-
pattern = re.compile(config.Get("PATTERN_KEYS"))
701+
pattern_keys = config.Get("PATTERN_KEYS")
702+
pattern_pub = config.Get("PATTERN_PUBKEYS")
703+
pattern_priv = config.Get("PATTERN_PRIVKEYS")
591704
found = collections.defaultdict(dict)
592705
for directory in directories:
593706
try:
@@ -597,28 +710,52 @@ def FindKeys(identity, config):
597710
continue
598711
raise
599712

600-
for key in keyfiles:
601-
key = os.path.join(directory, key)
602-
if not os.path.isfile(key):
713+
for keyname in keyfiles:
714+
keypath = os.path.join(directory, keyname)
715+
if not os.path.isfile(keypath):
603716
continue
604-
if not pattern.search(key):
717+
#
718+
match = None
719+
for pattern in pattern_keys:
720+
match = pattern.search(keyname)
721+
if match:
722+
break
723+
if match is None:
605724
continue
606-
607-
kinds = (
608-
("private", "priv"),
609-
("public", "pub"),
610-
(".pub", "pub"),
611-
("", "priv"),
612-
)
613-
for match, kind in kinds:
614-
if match in key:
615-
found[key.replace(match, "")][kind] = key
725+
#
726+
match = None
727+
if match is None:
728+
for pattern in pattern_pub:
729+
match = pattern[0].search(keyname)
730+
if match:
731+
kind = 'pub'
732+
break
733+
if match is None:
734+
for pattern in pattern_priv:
735+
match = pattern[0].search(keyname)
736+
if match:
737+
kind = 'priv'
738+
break
739+
if match is None:
740+
continue
741+
#
742+
if match and not pattern[1] is None:
743+
key = keyname[:match.start(pattern[1])] + keyname[match.end(pattern[1]):]
744+
else:
745+
key = keyname
746+
key = os.path.join(directory, key)
747+
found[key][kind] = keypath
616748

617749
if not found:
618750
print("Warning: no keys found for identity {0} in:".format(identity),
619751
file=sys.stderr,
620752
loglevel=LOG_WARN)
621753
print(directories, file=sys.stderr, loglevel=LOG_WARN)
754+
else:
755+
index = 0
756+
for keyname in found:
757+
index += 1
758+
print("Found key pair #{0} {1}: {2}{3}".format(index, keyname, found[keyname], " MISSING PUB" if not 'pub' in found[keyname] else " MISSING PRIV" if not 'priv' in found[keyname] else ""), file=sys.stderr, loglevel=LOG_DEBUG)
622759

623760
return found
624761

0 commit comments

Comments
 (0)