ssh-keygen empty output with subprocess.run and os.system in python3.10 on macOS Ventura 13.1

Question:

I have been attempting to output a signed public key generated from ssh-keygen (CLI) using python and the ‘subprocess’ library. I’ve also tried the ‘os’ library with the same results. I’m really looking to understand why it isn’t doing what I want: displaying the output of ‘ssh-keygen -Lf {keyfile}’ to the screen like I expect it to.

When I use this command on the same keyfile using the CLI (darwin / macOS Ventura 13.1) I get the expected results. When I use the command in Python3.10, I get a return-code of ‘1’ and no significant error to help me understand the problem. Please see below:

Expected results:

shell-prompt$> ssh-keygen -Lf ~/.ssh/signed_key.pub
/Users/USER/.ssh/signed_kali-os.pub:
        Type: [email protected] user certificate
        Public key: RSA-CERT SHA256:REDACTED
        Signing CA: RSA SHA256:REDACTED (using rsa-sha2-256)
        Key ID: "[email protected]"
        Serial: REDACTED
        Valid: from 2023-01-24T20:56:07 to 2023-01-24T21:01:37
        Principals: 
                principal-user
        Critical Options: (none)
        Extensions: 
                permit-pty

Output from basic Python3.10 script is either ‘1’ when printing return code, or empty (None) when printing stdout and/or stderr. I have tried all variations of printing that I can think of.

These are the two code solutions that I have attempted with no luck. When I replace the command (ssh-keygen) with something rudimentary like an ‘ls -l’ or ‘cat’, I get output as expected. I am confident that the variable signedPath works as expected because it works in other parts of the code not shown, and when I replace the variable with a hardcoded path it still fails.

1)
if sys.platform == "linux" or sys.platform == "linux2" or sys.platform == "darwin":
        keyOut = subprocess.run(['ssh-keygen','-Lf',signedPath],capture_output=True)
        print(keyOut.stdout.decode())
2)
os.system('ssh-keygen -Lf {key}'.format(key=signedPath))

What am I looking for? Ultimately, I would like to use this code to output the signed-public-key to the screen, because I like the output format that I get with ssh-keygen and I have had problems with other SSH key libraries in Python. If there is a better solution, I’d love to get some help with that, but I really am set on trying to understand why this specific code isn’t working, so I’d like an answer on that more than a separate solution. Any help here is greatly appreciated.

—+++ SOLVED +++—


Well…this is embarrassing…but I know it happens to all of us. I added a sleep for 5 seconds, and it works as expected now. Frustrating, as after my code exited the file is completed writing so I never knew it was 0-bytes during runtime, until I put more print statements in. The code I’m writing communicates with a vault server to sign my public key, and I wasn’t giving it time to complete, so python was trying to read a 0-byte file…I figured this out while making another minimal code sample.

—+++ SOLUTION +++—


If you encounter a similar problem, it may be that the file you’re trying to read is 0-bytes during run-time.

Asked By: bryan_stack

||

Answers:

This isn’t quite an answer — it’s not clear from your question why you’re not seeing the expected output — but I wanted to demonstrate what a minimal, complete, verifiable example ("MCVE") might look like, as well as provide a working example.

Consider this code, which (a) generates a key to use as a signing certificate, (b) generates a key to be signed, and then (c) signs the key from (b) with the key from (a):

#!/usr/bin/python

import os
import subprocess

expected_files = [
    "ca_key",
    "ca_key.pub",
    "user_key",
    "user_key.pub",
    "user_key-cert.pub",
]

for name in expected_files:
    try:
        os.remove(name)
    except FileNotFoundError:
        pass

# I'm using `check_output` -- and ignoring the return value -- in order to
# supress the output from these commands. This is effectively a shortcut
# for `subprocess.run` with `capture_output=True` and `check=True`.
print("generating keys")
subprocess.check_output(["ssh-keygen", "-t", "rsa", "-f", "ca_key", "-N", ""])
subprocess.check_output(["ssh-keygen", "-t", "rsa", "-f", "user_key", "-N", ""])
subprocess.check_output(
    ["ssh-keygen", "-s", "ca_key", "-I", "user_key", "user_key.pub"]
)

# Verify that we created everything we expected to create.
for name in expected_files:
    assert os.path.isfile(name)

# Here's the code from your question.
signedPath = "user_key-cert.pub"
keyOut = subprocess.run(["ssh-keygen", "-Lf", signedPath], capture_output=True)

print("keyOut:", keyOut.stdout.decode())

The above code is a self-contained example: it creates all the necessary files to run the code you’re asking about in your question; someone can imply copy the example, paste it into a file, and run it.

On my system (sys.platform == linux), running this example produces:

generating keys
Signed user key user_key-cert.pub: id "user_key" serial 0 valid forever
keyOut: user_key-cert.pub:
        Type: [email protected] user certificate
        Public key: RSA-CERT SHA256:Bca+gRvhx3twR0vFhIU3gr+TKIfFq2I1+lraJ6Z2QAI
        Signing CA: RSA SHA256:muz+6U5h9ZGyslm50m1F0VNG8VrseuQRzYwdUm2iGoo (using rsa-sha2-512)
        Key ID: "user_key"
        Serial: 0
        Valid: forever
        Principals: (none)
        Critical Options: (none)
        Extensions:
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

If you run the code on a Linux system and you see different behavior, I’m sure that with some additional details about your environment we can figure out what’s going on.

Answered By: larsks