Currently, we detect that a private SSH key in Passphrase is private by looking for the text "ENCRYPTED" in the key body. This is obviously unsophisticated, but it was effective for older keys.
It isn't effective for newer keys, which look like this:
$ ssh-keygen -N quack -f duck.key ... $ cat duck.key -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABAsWpnyxl ... SmXGo3NltrL7/e6+UVr6J94pSWCSA= -----END OPENSSH PRIVATE KEY-----
The key body is just a base64-encoded binary blob. It also doesn't base64-decode into anything that's obviously easy to parse -- the decoded blob has some pieces which are human-readable but is mostly actual binary data.
$ cat duck.base64 | base64 -D | hexdump -C 00000000 6f 70 65 6e 73 73 68 2d 6b 65 79 2d 76 31 00 00 |openssh-key-v1..| 00000010 00 00 0a 61 65 73 32 35 36 2d 63 74 72 00 00 00 |...aes256-ctr...| 00000020 06 62 63 72 79 70 74 00 00 00 18 00 00 00 10 2c |.bcrypt........,| 00000030 5a 99 f2 c6 56 9f 0c 04 69 7a cd b0 b8 01 29 00 |Z...V...iz....).| 00000040 00 00 10 00 00 00 01 00 00 01 17 00 00 00 07 73 |...............s| 00000050 73 68 2d 72 73 61 00 00 00 03 01 00 01 00 00 01 |sh-rsa..........| 00000060 01 00 d7 39 77 16 bb cd 4f 08 d5 fa 1e 42 6a ad |...9w...O....Bj.| ...
Although we might try to parse this some day and it seems like it's somewhat-standardized ("ASN.1"), I'm guessing this is a fair chunk of work. At the least, we know some keys exist in the wild with the "ENCRYPTED" header.
Short of decoding keys, we can mostly get the right answer by trying to change the password from "" (the empty string) into "" (the empty string), or by trying to extract the public key. If either one works works, the key is valid and has no password.
If this fails, the key might be invalid, or the key might have a password. Distinguishing between these cases without a parser isn't obviously easy, since the ways to test validity of a key (like using ssh-keygen -y to extract the public key) tend to fail in the presence of a password in the same way (same exit code, no machine-parseable output) that they fail in the presence of a malformed key.
In theory, we could distinguish between these cases by using SSH_ASKPASS=xyz ssh-keygen ..., where xyz is some binary which causes a side effect so we can tell whether ssh-keygen tried to prompt us for a password or not. This doesn't actually seem to work, but some searching suggests it might be because SSH_ASKPASS is ignored if stdin is a TTY.
Tricking ssh-keygen into actually using SSH_ASKPASS in a repeatable way seems to involve this:
$ echo '' | SSH_ASKPASS=/usr/bin/false DISPLAY= ssh-keygen -y -f duck.key
But how can we robustly determine if ssh-keygen tried to prompt or not? We could make /usr/bin/false some arbitrarily complex mess, but SSH_ASKPASS doesn't seem to support arguments. Maybe we can pass them through the environment.
ssh-keygen appears to echo stderr through, so we end up with this as a possible concoction:
#!/usr/bin/env php <?php fprintf(STDERR, "<!!!>\n");
$ echo '' | DISPLAY= SSH_ASKPASS=/Users/epriestley/scratch/key-test/ping-stderr.php ssh-keygen -y -f duck.key 2> askpass.log $ cat askpass.log <!!!> Load key "duck.key": incorrect passphrase supplied to decrypt private key
That is:
- Write a binary which writes a known message to stderr.
- Pass DISPLAY= and SSH_ASKPASS=/path/to/that/binary to ssh-keygen -y ... or ssh-keygen -p ... with stdin not a TTY.
- Capture stderr.
- Look for our magic message to distinguish between a bad passphrase (message present) and a bad key format (message absent).
This is ridiculous and fragile so I'm not going to deal with it for now, but it's at least a possible way forward.
The advantage of using ssh -p ... (change password) over ssh -y ... is that ssh -p has a -P flag to pass the password, but this actually works fine for ssh -y too.
Splitting the difference, we can do this instead:
- Use ssh -y -P '' -f /path/to/key to guess validity/passwordiness in one fell swoop
- If that works, great: valid key with no password.
- If that fails, check stderr.
- If it matches "incorrect passphrase supplied to decrypt private key", assume passphrase issue.
- If it matches "invalid format", assume format issue.
- If it matches neither (e.g., the installed version of ssh-keygen has different messages than my local version of ssh-keygen), use ambiguous messaging ("might be invalid, might have a password").