Skip to Content [alt-c]

November 12, 2021

It's Now Possible To Sign Arbitrary Data With Your SSH Keys

Did you know that you can use the ssh-keygen command to sign and verify signatures on arbitrary data, like files and software releases? Although this feature isn't super new - it was added in 2019 with OpenSSH 8.0 - it seems to be little-known. That's a shame because it's super useful and the most viable alternative to PGP for signing data. If you're currently using PGP to sign data, you should consider switching to SSH signatures.

Here's why I like SSH signatures:

  • It's not PGP. For years, security professionals have been sounding the alarm on PGP, including its most popular implementation, GnuPG/GPG. PGP is absurdly complex, has an awful user experience, and is full of crufty old cryptography which shouldn't be touched with a ten foot pole.

  • SSH is everywhere, and people already have SSH keys. If you use Debian Bullseye or Ubuntu 20.04 or newer, you already have a new enough version of SSH installed. And if you use GitHub, or any other service that uses SSH keys for authentication, you already have an SSH key that can be used to generate signatures. This is why I'm more excited about SSH signatures than other PGP signature alternatives like signify or minisign. Signify and minisign are great, but require you to install new software and generate new keys, which will hinder widespread adoption.

  • SSH key distribution is easy. SSH public keys are one line strings that are easy to copy around. You don't need to use the Web of Trust or worry about configuring "trust levels" for keys. GitHub already acts as a key distribution service which is far easier to use and more secure than any of the PGP key servers ever were. You can retrieve the SSH public keys for any GitHub user by visiting a URL like https://github.com/USERNAME.keys. (For example, my public keys are at https://github.com/AGWA.keys.)

    (GitHub acts as a trusted third party here, and you have to trust them not to lie about people's public keys, so it may not be appropriate for all use cases. But relying on a trusted third party with a professional security team like GitHub seems like a way better default than PGP's Web of Trust, which was nigh impossible to use. Key Transparency would address the concerns with trusted third parties, if anyone ever figures out how to audit transparency logs in practice.)

  • SSH has optional lightweight certificates. You don't have to use SSH certificates (and most people shouldn't) but if certificates would make your life easier, SSH has a lightweight certificate system that is considerably simpler than X.509. This makes SSH signatures a good alternative to S/MIME as well!

You'll soon be able to sign Git commits and tags with SSH

Signing Git commits and tags gives consumers of your repository assurance that your code hasn't been tampered with. Unfortunately, you currently have to use either PGP or S/MIME, and personally I haven't bothered to sign Git tags since my PGP keys expired in 2018.

But that will soon change in Git 2.34, which adds support for SSH signatures.

Signing files

Signing a file is straightforward:

ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n file file_to_sign

Here are the arguments you may need to change:

  • ~/.ssh/id_ed25519 is the path to your private key. This is the standard path to your SSH Ed25519 private key. If you have an RSA key, use id_rsa instead.

  • file is the "namespace", which describes the purpose of the signature. SSH defines file for signing generic files, and email for signing emails. Git uses git for its signatures.

    If you are using the signature for a different purpose, such as a custom protocol, you must specify your own namespace. This prevents cross-protocol attacks whereby a valid signature is removed from a message for one protocol and attached to a message from a different protocol. If the protocols don't use distinct namespaces for their signatures, there's a risk that the signature is considered valid by the second protocol even though it was meant for the first protocol.

    Namespaces can be arbitrary strings. To ensure global uniqueness of namespaces, SSH recommends that you structure them like an email address under a domain that you own. For example, I would use a namespace like protocolname-v1@agwa.name.

  • file_to_sign is the path to the file to be signed.

The signature is written to a new file called file_to_sign.sig, which looks like this:

-----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAg2rirQQddpzEzOZwbtM0LUMmlLG krl2EkDq4CVn/Hw7sAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx OQAAAEDyjWPjmOdG8HJ8gh1CbM8WDDWoGfm+TTd8Qa8eua9Bt5Cc+43S24i/JqVWmk98qV YXoQmOYL4bY8t/q7cSNeMH -----END SSH SIGNATURE-----

If you specify - for the filename, the file to sign is read from standard in and the signature is written to standard out.

Verifying signatures

Verifying signatures is a bit more involved. First you need to create an allowed signers file which maps email addresses to public keys, like this:

alice@example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINq4q0EHXacxMzmcG7TNC1DJpSxpK5dhJA6uAlZ/x8O7 alice@example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCfHGCK5jjI/Oib4vRBLB9rG30A8y/Br9U75rfAYsitwFPFfl/CaTAvfRlW1lIBqOCshLWxGsN+PFiJCiCWzpW4iILkD5X5KcBBYHTq1ojYXb70BrQXQ+QBDcGxqQjcOp/uTq1D9Z82mYq/usI5wdz6f1KNyqM0J6ZwRXMu6u7NZaAwmY7j1fV4DRiYdmIfUDIyEdqX4a1Gan+EMSanVUYDcNmeBURqmTkkOPYSg8g5xYgcXBMOZ+V0ZUjreV9paKraUD/mVDlZbb/VyWhJGT4FLMNXHU6UHC2FFgqANMUKIlL4vhqc23MoygKbfF3HgNB6BNfv3s+GYlaQ3+66jc5j bob@example.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBgQuuEvhUXerOTIZ2zoOx60M/HHJ/tcHnD84ZvTiX5b eve@example.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxsKcWHB9hamTXCPWKVUw0WM0S3IXH0YArf8iJE0dMG

Once you have your allowed signers file, verification works like this:

ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s file_to_verify.sig < file_to_verify

Here are the arguments you may need to change:

  • allowed_signers is the path to the allowed signers file.

  • alice@example.com is the email address of the person who allegedly signed the file. This email address is looked up in the allowed signers file to get possible public keys.

  • file is the "namespace", which must match the namespace used for signing as described above.

  • file_to_verify.sig is the path to the signature file.

  • file_to_verify is the path to the file to be verified. Note that this file is read from standard in. In the above command, the < shell operator is used to redirect standard in from this file.

If the signature is valid, the command exits with status 0 and prints a message like this:

Good "file" signature for alice@example.com with ED25519 key SHA256:ZGa8RztddW4kE2XKPPsP9ZYC7JnMObs6yZzyxg8xZSk

Otherwise, the command exits with a non-zero status and prints an error message.

Is it safe to repurpose SSH keys?

Short answer: yes.

Always be wary of repurposing cryptographic keys for a different protocol. If not done carefully, there's a risk of cross-protocol attacks. For example, if the structure of the messages signed by Git is similar to the structure of SSH protocol messages, an attacker might be able to forge Git artifacts by misappropriating the signature from an SSH transcript.

Fortunately, the structure of SSH protocol messages and the structure of messages signed by ssh-keygen are dissimilar enough that there is no risk of confusion.

To convince ourselves, let's consult RFC 4252 section 7, which specifies how SSH keys are traditionally used by SSH to authenticate a user logging into a server. The RFC specifies that the input to the signature algorithm has the following structure:

string session identifier byte SSH_MSG_USERAUTH_REQUEST string user name string service name string "publickey" boolean TRUE string public key algorithm name string public key to be used for authentication

The first field is the session identifier, a string. In the SSH protocol, strings are prefixed by a 32-bit big endian length. The session identifier is a hash. Since hashes are short, the first three bytes of the above signature input will always be zero.

Meanwhile, the PROTOCOL.sshsig file the OpenSSH repository specifies how SSH keys are used with ssh-keygen-generated signatures. It specifies that the input to the signature algorithm has this structure:

#define MAGIC_PREAMBLE "SSHSIG" byte[6] MAGIC_PREAMBLE string namespace string reserved string hash_algorithm string H(message)

Here, the first three bytes are SSH, from the magic preamble. Since the first three bytes of the SSH protocol signature input are different from the ssh-keygen signature input, the SSH client and ssh-keygen will never produce identical signatures. Therefore, there is no risk of cross-protocol attacks, and I am totally comfortable using my existing SSH keys to sign messages with ssh-keygen.

Comments

Reader Charlie on 2021-11-12 at 18:45:

You can actually use openssl with RSA keys generated by ssh-keygen to sign also, and this has worked for a long time.

https://www.linuxjournal.com/content/flat-file-encryption-openssl-and-gpg

You will have to generate an openssl-compatible public key:

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

To sign:

openssl dgst -sha256 -sign ~/.ssh/id_rsa -out known_hosts.sha256 known_hosts

To verify:

openssl dgst -sha256 -verify ~/.ssh/id_rsa.pub.openssl -signature known_hosts.sha256 known_hosts

Reply

Andrew Ayer on 2021-11-12 at 21:32:

The problem with using openssl is that you have to worry about cross-protocol attacks because there is no namespace parameter like there is with SSH signatures. SSH signatures provide the necessary structure to safely use a single key for multiple purposes.

Reply

Reader Artiom on 2021-11-13 at 16:47:

Is it possible to sign but providing the private key via an SSH agent instead of a file? In other words without the "-f ~/.ssh/id_ed25519"

For example KeePass can act as an SSH agent, and the user won't have any private keys on disk. Other users may have their own SSH agents.

Reply

Andrew Ayer on 2021-11-13 at 17:24:

Yup, if you specify the path to a public key via -f, then it will use the SSH agent to make the signature.

Reply

Anonymous on 2021-11-13 at 20:36:

Oh I see, so one way or another, a file must exist on disk, and must be specified in the -f argument. It isn't possible to purely use an SSH agent.

Reply

Reader Robert Irelan on 2022-08-25 at 23:22:

You can get around that with Bash process substitution:

``` $ ssh-keygen -Y sign -f <(ssh-add -L | grep YOUR_KEY) -n file foo.html Signing file foo.html Write signature to foo.html.sig ```

Reply

Reader Robert Irelan on 2022-08-25 at 23:23:

I mean

$ ssh-keygen -Y sign -f <(ssh-add -L) -n file foo.html Signing file foo.html Write signature to foo.html.sig

Reply

Reader Vee on 2021-11-21 at 16:16:

How about encrypting/decrypting a file/folder using only SSH?

Reply

Andrew Ayer on 2021-11-22 at 17:54:

For file encryption, I recommend age: https://github.com/FiloSottile/age

age lets you encrypt using an SSH key, though I generally recommend against encrypting to long-term keys like SSH keys because if the key gets compromised it risks exposing anything ever encrypted to that key. Instead, I recommend frequently generating new age encryption keys for specific, short-lived purposes.

Reply

Reader Vee on 2021-11-23 at 16:01:

Thank you!

Reply

Reader Johnny on 2022-01-06 at 19:05:

Remind me again, what's the proper order here? Encrypt then sign seems logical to me, but I've heard a lot of folks recommend sign then encrypt as well. Thanks.

Reply

Reader Carlos Lint on 2022-03-03 at 03:37:

Always verify the signature first, if that doesn't match drop the message/file/context altogether. Think of it as a network packet received with bad checksum: Why bother in the first place?

Reply

Anonymous on 2023-10-24 at 15:36:

It's good that OpenSSH team added the signing but I wouldn't use it: - Inconvinient command line. Why the signing tool is called ssh-KEYGEN? Arguments are difficult to understand. The allowed signers file should be juset created somewhere in a default location. - Again as with signify the OpenSSH team decided to use own format instead of standard PKCS#7. Extensions of signatires aren't specified. Here you used .sig but this is already used by PGP and other programs like Kleopatra will be unable to open it. The cryptogtpgaphy needs a wide adoption but here fragmentaions only increased. - namespaces is somemthing really weird and just won't be used in wild.

Reply

Reader JPG on 2024-10-11 at 16:02:

Perhaps you could create a short bash alias that wraps the commands for you so that you can feel safe and secure? I believe this has been a feature of shells since 1997? .sig is used by lots of other applications too. At least three are listed on https://fileinfo.com/extension/sig and none of them are PGP or Kleopatra.

PKCS#7 is fine but old and nothing supports it other than Windows to store key data in--not for signatures. It was never widely adopted for signatures and I can't think of a single library that supports it.

This is widely used by GitHub now for signing as an alternative to PGP. The namespacing in particular is important as git uses a specific namespace and thus isn't weird and is definitely being used now "in the wild."

Reply

Post a Comment

Your comment will be public. To contact me privately, email me. Please keep your comment polite, on-topic, and comprehensible. Your comment may be held for moderation before being published.

(Optional; will be published)

(Optional; will not be published)

(Optional; will be published)

  • Blank lines separate paragraphs.
  • Lines starting with > are indented as block quotes.
  • Lines starting with two spaces are reproduced verbatim (good for code).
  • Text surrounded by *asterisks* is italicized.
  • Text surrounded by `back ticks` is monospaced.
  • URLs are turned into links.
  • Use the Preview button to check your formatting.