Is it possible to sign a file using an ssh key?

Solution 1:

There may not be a way to do this with the OpenSSH tools alone.

But it can be done quite easily with the OpenSSL tools. In fact, there are at least two ways to do it. In the examples below, ~/.ssh/id_rsa is your private key.

One way is using dgst:

openssl dgst -sign ~/.ssh/id_rsa some-file

The other is using pkeyutl:

openssl pkeyutl -sign -inkey ~/.ssh/id_rsa -in some-file

Both of these write a binary signature to standard output. dgst takes a -hex option will print a textual representation, with some details about the form of the signature. pkeyutl takes a -hexdump option which is a bit less useful. Both will accept both RSA and DSA keys. I have no idea what the format of the output is. The two commands produce different formats. I get the impression that pkeyutl is considered more modern than dgst.

To verify those signatures:

openssl dgst -verify $PUBLIC_KEY_FILE -signature signature-file some-file

and:

openssl pkeyutl -verify -inkey $PUBLIC_KEY_FILE -sigfile signature-file -in some-file

The problem here is $PUBLIC_KEY_FILE. OpenSSL can't read OpenSSH's public key format, so you can't just use id_rsa.pub. You have a few options, none ideal.

If you have a version of OpenSSH of 5.6 or later, you can apparently do this:

ssh-keygen -e -f ~/.ssh/id_rsa.pub -m pem

Which will write the public key to standard output in PEM format, which OpenSSL can read.

If you have the private key, and it's an RSA key, then you can extract the public key from it (I assume the PEM-encoded private key file includes a copy of the public key, since it is not possible to derive the public key from the private key itself), and use that:

openssl rsa -in ~/.ssh/id_rsa -pubout

I don't know if there's a DSA equivalent. Note that this approach requires some cooperation from the owner of the private key, who will have to extract the public key and send it to the would-be verifier.

Lastly, you can use a Python program written by a chap called Lars to convert the public key from OpenSSH to OpenSSL format.

Solution 2:

I stumpled upon this old post looking for the same thing. As it turns out, ssh-keygen from the OpenSSH tools is nowadays directly capable of generating and validating signatures using existing SSH keys. This has been introduced in OpenSSH 8.1 (released on 2019-10-09).

TL;DR

Use ssh-keygen to sign and verify signatures:

echo "Hello, World!" | ssh-keygen -Y sign -n file -f id_rsa > content.txt.sig
echo "Hello, World!" | ssh-keygen -Y check-novalidate -n file -f id_rsa.pub -s content.txt.sig

Arguments for ssh-keygen

As I didn't find the ssh-keygen man page to be particularly helpful (yet), here is an overview of a few useful commands and their required arguments for signing content.

Signing content:

ssh-keygen 
-Y sign        # The 'sign' signature operation
-n <namespace> # Signature namespace (e.g. file or email)
-f <file>      # Path to the private key to sign the content with
<files>        # Paths to one or more files you want to sign (optional, by default reads from stdin)

Checking the signature of signed content:

ssh-keygen 
-Y check-novalidate # The 'check' signature operation
-n <namespace>      # Namespace the signature was generated in
-f <file>           # Path to the public key to validate the signature with
-s <file>           # Path to the signature file

Check signature and verify whether the signer is authorized to sign the content:

ssh-keygen 
-Y verify      # The 'verify' signature operation
-n <namespace> # Namespace the signature was generated in
-f <file>      # Path to the ALLOWED SIGNERS file
-s <file>      # Path to the signature file
-I <principal> # The expected identity principal used to generate the signature

Search the ALLOWED SIGNERS file for any principals that are applicable for the specified signature:

ssh-keygen 
-Y find-principals # The 'find principals' signature operation
-f <file>          # Path to the ALLOWED SIGNERS file
-s <file>          # Path to the signature file

Display your key's principal (usually <username>@<hostname> from when it was generated):

ssh-keygen 
-l        # Print the key's fingerprint
-f <file> # Path to the public key 

Generate a user certificate from an Certificate Authority (CA) SSH key:

ssh-keygen 
-I <cert_id>    # The certificate identity
-s <file>       # Path to the CA's private key
-n <principals> # Identity principals allowed by the generated certificate
<files>         # Path to one or more public keys to generate a certificate for

The ALLOWED SIGNERS file

This file contains a list of identities which are allowed to sign content, and is therefore used to determine whether a signature comes from an authorized source. The format is similar to the well-known authorized_keys file, and described in more detail in the ALLOWED SIGNERS section in the man page.

<princpal>[,<principal>,..] [cert-authority] [namespaces="<namespace>[,<namespace>,..]"] <public-key>

Examples:

user1@server,user2@server ssh-rsa AAAAX1...
*@server cert-authority ssh-rsa AAAAX1...

Using the signature operation commands

The simplest way of generating a signature and validating it:

# Generate a new SSH key
ssh-keygen -f id_rsa -N ''

# Create the original content to be signed
echo "Trust me, the author!" > content.txt

# Sign the content
cat content.txt | ssh-keygen -Y sign -n file -f id_rsa > content.txt.sig

# Check the signature of the content
cat content.txt | ssh-keygen -Y check-novalidate -f id_rsa.pub -n file -s content.txt.sig

Now try to check the signature with invalid content:

# Pretend the content has been tampered with by a malicious party
echo "Trust ME, the hacker!" > content-tampered.txt

# Check the signature of the content again (which will fail)
cat content-tampered.txt | ssh-keygen -Y check-novalidate -f id_rsa.pub -n file -s content.txt.sig

Note that the signature has been verified using the check-novalidate signature operation. The novalidate part sounds a little scary when you want to validate whether the signature matches with the content. However, when this check succeeds, it does mean that the signature matches with the original content. You just have no information about whether the signer was authorized to sign the content.

When you have a public key you trust and expect to be used to sign the content, you can make use of the ALLOWED SIGNERS file:

# Export your key's principal
ssh-keygen -l -f id_rsa.pub | cut -d ' ' -f3 > identity

# Create an ALLOWED SIGNERS file
echo "$(cat identity) $(cat id_rsa.pub)" > allowed_signers

# Check signature and verify the signer 
cat content.txt | ssh-keygen -Y verify -f allowed_signers -I $(cat identity) -n file -s content.txt.sig

As you already might have seen in the overview above, OpenSSH is nowadays also capable of signing other SSH keys. This means we can use the SSH key to act as a Certificate Authority (CA), which is pretty cool!

What this allows you to do for signing is only having to specify the CA's public key in the ALLOWED SIGNERS file, while all SSH keys signed by the CA are capable of signing valid content:

# Generate a new CA key
ssh-keygen -f ca -C 'SSH Certificate Authority' -N ''

# Replace the ALLOWED SIGNERS file contents with just the CA's public key
echo "$(cat identity) cert-authority $(cat ca.pub)" > allowed_signers

# Sign your public key using the CA, creating a user certificate
ssh-keygen -I $(cat identity) -s ca -n $(cat identity) id_rsa

# Sign the content by specifying the user certificate (which uses the private key)
cat content.txt | ssh-keygen -Y sign -n file -f id_rsa-cert.pub > content.txt.sig

# Validate the signature using the CA's public key
cat content.txt | ssh-keygen -Y verify -f allowed_signers -I $(cat identity) -n file -s content.txt.sig

Now that we have a CA, we can also make use of custom arbitrary principals, as they are embedded in the certificate

# Sign your public key using the CA, adding a custom principal
ssh-keygen -I $(cat identity) -s ca -n $(cat identity),trusted-authors id_rsa

# Replace the ALLOWED SIGNERS file contents, allowing only signatures generated by keys with the trusted-authors principal
echo "trusted-authors cert-authority $(cat ca.pub)" > allowed_signers

# Sign the content by specifying the user certificate 
cat content.txt | ssh-keygen -Y sign -n file -f id_rsa-cert.pub > content.txt.sig

# Validate the signature using the CA's public key and the expected custom principal
cat content.txt | ssh-keygen -Y verify -f allowed_signers -I trusted-authors -n file -s content.txt.sig

Resulting files

If you've followed along with the commands above, you'll end up with these files in your directory:

allowed_signers      # List of principal(s) and key combinations authorized to sign the content 
ca                   # Private key of the Certificate Authority
ca.pub               # Public key of the Certificate Authority
content-tampered.txt # The content that got tampered with
content.txt          # The original content
content.txt.sig      # A signature of the original content
id_rsa               # Our private key - used to generate the signature
id_rsa-cert.pub      # Certificate signed by the CA - used to check and verify the signature
id_rsa.pub           # Our public key  - used to check the signature
identity             # Helper file which contains the identity principal of our private key

Hope it helps!