This tool signs and verifies data using the
SSHSIG
protocol, as implemented by ssh-keygen -Y sign and ssh-keygen -Y verify.
Compared to ssh-keygen, which has a lot of features, most of which are not
related to SSHSIG itself, ssh-sign is dedicated to SSHSIG. The main goal is
to provide a simpler user interface, more informative output/error messages,
including optional JSON output for scripting purposes.
The SSHSIG protocol is implemented in pkg/sshsig using
golang.org/x/crypto/ssh and the standard library. Compatibility is tested by
signing and verifying with both ssh-sign and ssh-keygen.
ssh-sign exits 0 when the command succeeds, 1 on runtime errors and 2
on user errors such as bad usage.
In JSON mode, runtime errors are written to stdout under error before exiting
1.
On Linux, each command opens its inputs and then uses Landlock ABI V10 via
go-landlock
to deny new filesystem access, execution, TCP and UDP networking, and access
to external pathname and abstract unix sockets. Existing file descriptors
remain usable. sign therefore connects to SSH_AUTH_SOCK before restricting
itself.
Restriction is automatic and best effort. Unsupported kernels, disabled Landlock, other operating systems, and an unavailable or erroring ABI query continue without restriction.
An optional config file will be read from $SSH_SIGN_CONFIG_DIR/config
(~/.ssh/sign/config by default).
[sign]
key = work
[alias]
work = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk
user@example.com = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDa41u/FI8kReoBAPIIzt11NwF00HIytf1fUsUuP9csA| option | meaning |
|---|---|
sign.key |
public key or alias to use when ssh-sign sign is run without --sign-key |
alias.<name> |
public key referenced by --auth-key, --sign-key or sign.key |
Alias names may contain ASCII letters, digits and . _ % + - @, like email
addresses.
Section and option names are case-sensitive, and no quoting is used for values.
Empty lines and lines starting with # are ignored.
verify and check reports various information and result fields:
| field | meaning |
|---|---|
principal |
the identity specified with -p (verify only) |
authentication |
result of checking the signer against the specified principal: valid, invalid or disabled |
namespace |
the signature's namespace (verify only; run inspect to view it when using check) |
designation |
result of checking the namespace against -n or allowed signers: valid, invalid or disabled |
verification |
result of verifying the signature: valid or invalid |
entry |
with verify, the matched allowed signers entry as file:line |
disabled means that the corresponding check was not requested. With check,
-K disables authentication and -N disables designation. With verify,
authentication is disabled when -p is omitted, although the key must still
match an allowed signers entry. designation is disabled only with -N; a
matching namespaces= restriction instead makes it valid.
Some values are colored when stdout is a TTY, valid becomes green and
disabled becomes yellow. In plain-text mode, the invalid value is never
displayed since it results in an error message on stderr (which is colored red,
if stderr is a TTY). JSON output is never colored. Set the NO_COLOR
environment variable to a non-empty value to disable all colors.
$ ssh-sign --help
usage: ssh-sign <command> [command option]..
commands:
inspect Show signature details
sign Sign data with specified public key and namespace
verify Verify signed data using allowed signers files
check Check signed data, with optional public key/namespace validation
version Show version
Every command also accepts:
-j, --json enable JSON output
-v, --verbose write debug output to stderr, repeatable up to -vvv
-v writes debug output to stderr. Repeat it for more detail, up to -vvv:
| level | contents |
|---|---|
-v |
what is signed or verified, the agent connection and the outcome of each check |
-vv |
which files are read, the parsed allowed signers count and the agent key listing |
-vvv |
every allowed signers entry with its constraints, and every key the agent offers |
$ ssh-sign sign -vv -k "$(cat key.pub)" -f data > data.sig
debug1: running command command=sign json=false
debug2: reading input role=data path=data
debug1: sign: signing namespace=file key.type=ssh-ed25519 key.fingerprint=SHA256:LyEOd2jBAC/rJ6fwD0FVOBJTLJsKorRx5bjfBeZ0FM8
debug1: agent: connecting socket=/run/user/1000/keyring/ssh timeout=30s
debug1: agent: connected socket=/run/user/1000/keyring/ssh
debug2: agent: listing keys timeout=30s
debug2: agent: listed keys keys=1
debug1: agent: matched key type=ssh-ed25519 fingerprint=SHA256:LyEOd2jBAC/rJ6fwD0FVOBJTLJsKorRx5bjfBeZ0FM8
debug1: sign: signed format=ssh-ed25519 hash=sha512
sign creates an SSHSIG digital signature of given data.
-f, --data-file read data to sign from file instead of stdin
-n, --namespace create signature with specified namespace (default "file")
-k, --sign-key create signature using this pubkey reference or config alias
$ ssh-add -L
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk user@localhost
$ echo test | ssh-sign sign -k 'AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk'
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAggLk2JJEthHz9x97Tprql
YfE157k0fPtVv76pglLTq6QAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtz
c2gtZWQyNTUxOQAAAEC4II0w5rOL2caBB33l3482sbG7fCkfG5yHWAFNl+hRTLVz
ErHRKw6biwpo2ZeYpEvmFQAxqn5iFWczak8drGAM
-----END SSH SIGNATURE-----
$ echo test > data
$ ssh-sign sign -k 'AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk' -f data > data.sig
$ cat data.sig
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAggLk2JJEthHz9x97Tprql
YfE157k0fPtVv76pglLTq6QAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtz
c2gtZWQyNTUxOQAAAEC4II0w5rOL2caBB33l3482sbG7fCkfG5yHWAFNl+hRTLVz
ErHRKw6biwpo2ZeYpEvmFQAxqn5iFWczak8drGAM
-----END SSH SIGNATURE-----
Note
The public key specified on the command line is a reference to a pubkey that
will be searched for in the SSH agent (env SSH_AUTH_SOCK). Make sure that
the pubkey shows up in ssh-add -L.
Specifying keyfiles directly is not supported. After all, you should be using a hardware token for your SSH key anyway, and therefore an agent of some kind.
Agent connection and key listing time out after 30 seconds. Signing has no timeout because hardware keys may wait for a touch or PIN.
inspect generates signature metadata.
-s, --signature-file read signature from file instead of stdin
inspect, check, and verify accept trailing ASCII whitespace in
signatures, including an extra newline from terminal pasting. When the
signature comes from stdin without -s, they also accept up to 1024 bytes of
whitespace-only lines before the header. This leading whitespace allowance
extends the SSHSIG format and ssh-keygen behavior. Signature files must still
start with the header. Unlike ssh-keygen, ssh-sign rejects non-whitespace
text after the END line.
$ ssh-sign inspect < data.sig
version | 1
publickey_format | ssh-ed25519
publickey_blob | AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk
publickey_fingerprint | SHA256:7f4G0lT+fU/dDnPDfQd1wmQVPUxYvZm+ZNQqVJtlqNk
namespace | file
hash_algorithm | sha512
signature_format | ssh-ed25519
signature_blob | uCCNMOazi9nGgQd95d+PNrGxu3wpHxuch1gBTZfoUUy1cxKx0SsOm4sKaNmXmKRL5hUAMap+YhVnM2pPHaxgDA==
Alternatively with JSON output via e.g. ssh-sign inspect -j -s data.sig:
{
"result": {
"version": "1",
"public_key": {
"format": "ssh-ed25519",
"blob": "AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk",
"fingerprint": "SHA256:7f4G0lT+fU/dDnPDfQd1wmQVPUxYvZm+ZNQqVJtlqNk"
},
"namespace": "file",
"hash_algorithm": "sha512",
"signature": {
"format": "ssh-ed25519",
"blob": "uCCNMOazi9nGgQd95d+PNrGxu3wpHxuch1gBTZfoUUy1cxKx0SsOm4sKaNmXmKRL5hUAMap+YhVnM2pPHaxgDA=="
}
}
}A signature from a FIDO security key carries flags and a counter after the
signature itself. inspect reports these as securitykey_ rows, or as
signature.security_key in JSON. It also shows the key's application as
publickey_application in text and public_key.application in JSON:
publickey_application | ssh:
securitykey_flags | 0x05 (presence,user-verified)
securitykey_counter | 13
The raw byte is kept alongside the names, so a flag this tool does not decode
is still visible. JSON reports flags, user_presence, user_verification,
backup_eligible, backed_up and counter separately.
Note
inspect reads these fields without verifying the signature. Verification
shows that the flags and counter were signed by the matching private key,
but cannot prove that the key is genuine hardware or that a touch happened.
The presence flag is the signer's report of a user-presence check. A key
created with no-touch-required can sign without that check and may show
0x00 (none) when no flags are set.
check verifies signatures without requiring an allowed signers file.
-f, --verify-file read data to verify from file (required)
-s, --signature-file read signature from file (default stdin, or <verify-file>.sig when stdin=tty)
-n, --namespace require a signature with specified namespace (default "file")
-N, --no-namespace accept a signature with any namespace
-k, --auth-key require a signature created by this public key or config alias
-K, --no-auth-key accept a signature created by any public key
-t, --timestamp validate this RFC3339/RFC1123 timestamp rather than current time
Without -s, check (and also verify) reads the signature from stdin, or
from <verify-file>.sig if stdin is a terminal.
Note
For check, -t only affects certificate validity.
ssh-sign check -f data -s data.sig -KN
authentication = disabled
designation = disabled
verification = valid
$ ssh-sign check -f data -s data.sig -Kn file
authentication = disabled
designation = valid
verification = valid
$ ssh-sign check -f data -s data.sig -k AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk -n file
authentication = valid
designation = valid
verification = valid
$ ssh-sign check -f data -s data.sig -k AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS0abc -n abc
error: check: signature was created by public key "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk" (expected "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS0abc")
error: check: signature contains namespace "file" (expected "abc")
Enabling JSON output for the last one gives:
{
"error": [
"signature was created by public key \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk\" (expected \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS0abc\")",
"signature contains namespace \"file\" (expected \"abc\")"
],
"result": {
"authentication": "invalid",
"designation": "invalid",
"verification": "valid"
}
}verify verifies signatures by parsing an allowed signers file.
-a, --allowed-signers-file read allowed signers, with options, from file
-f, --verify-file read data to verify from file (required)
-s, --signature-file read signature from file (default stdin, or <verify-file>.sig when stdin=tty)
-n, --namespace require a signature with specified namespace
-N, --no-namespace ignore the signature namespace and allowed signers namespace restrictions
-p, --principal allow this signer (email usually) from allowed signers file
-t, --timestamp validate this RFC3339/RFC1123 timestamp rather than current time
Important
If neither -n nor -N is given, the signature's namespace must match a
namespaces= restriction in an applicable allowed signers entry. Entries
with namespace restrictions take precedence over unrestricted entries,
regardless of their order. Verification fails if no restriction matches.
Use -n when the expected namespace is known independently. -N disables
all namespace checks, including restrictions in the allowed signers file.
Key, principal and time checks still apply.
Note
authentication indicates whether the signer is authorized for the
principal specified with -p. If -p is omitted, authentication is
disabled and principal contains the pattern-list from the matching allowed
signers entry. The signer's key must still appear in the allowed signers file.
Note
The file format and cases where this parser is deliberately stricter than
ssh-keygen are documented in
doc/ALLOWED_SIGNERS.md. A malformed line is skipped
rather than fatal; use -v to list what was skipped.
Note
A -t timestamp without a time zone and an allowed-signers timestamp without
a trailing Z are interpreted in the local time zone, matching ssh-keygen.
For reproducible CI results, include an RFC3339 offset in the -t value and
append Z to timestamps in the allowed signers file.
When ssh-sign verify is run without --allowed-signers-file, all *.conf
files in $SSH_SIGN_CONFIG_DIR/signers/ (~/.ssh/sign/signers/ by default)
are loaded in lexical order, as if they were concatenated as a single allowed
signers file. The total size of the files must not exceed 64 MiB.
$ echo 'test1@localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk' > allowed_signers
$ ssh-sign verify -a allowed_signers -f data < data.sig
error: verify: signature namespace "file" was left unverified: no namespace was requested and no matching allowed signers entry restricts one (use -n, -N or namespaces=)
$ ssh-sign verify -a allowed_signers -f data -n file -p test1@localhost < data.sig
principal = test1@localhost
authentication = valid
namespace = file
designation = valid
verification = valid
entry = allowed_signers:1
$ ssh-sign verify -a allowed_signers -f data -n file -p test2@localhost < data.sig
error: verify: principal "test2@localhost" not found within allowed signers
$ echo 'test1@localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS0abc' > allowed_signers
$ ssh-sign verify -a allowed_signers -f data < data.sig
error: verify: signer public key not found within allowed signers
$ echo 'test1@localhost namespaces="abc" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk' > allowed_signers
$ ssh-sign verify -a allowed_signers -f data -p test1@localhost < data.sig
error: verify: principal "test1@localhost" found in allowed signers, but failed constraints: file="allowed_signers" line=1: namespace mismatch
$ echo 'test1@localhost namespaces="file",valid-after="20260101",valid-before="20260201" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC5NiSRLYR8/cfe06a6pWHxNee5NHz7Vb++qYJS06uk' > allowed_signers
$ date
Tue Feb 3 22:49:14 UTC 2026
$ ssh-sign verify -a allowed_signers -f data -p test1@localhost < data.sig
error: verify: principal "test1@localhost" found in allowed signers, but failed constraints: file="allowed_signers" line=1: expired
$ ssh-sign verify -a allowed_signers -f data -p test1@localhost -t 2026-01-15 < data.sig
principal = test1@localhost
authentication = valid
namespace = file
designation = valid
verification = valid
entry = allowed_signers:1
$ ssh-sign verify -a allowed_signers -f data -p test1@localhost -t 2025-12-31 < data.sig
error: verify: principal "test1@localhost" found in allowed signers, but failed constraints: file="allowed_signers" line=1: not yet valid
JSON output for a successful verification with -n file:
{
"result": {
"principal": "test1@localhost",
"authentication": "valid",
"namespace": "file",
"designation": "valid",
"verification": "valid",
"entry": {
"file": "allowed_signers",
"line": 1
}
}
}version shows the version. make sets it from git describe. Other builds
use the module version Go records, or unknown if there is none.
$ ssh-sign version
v0.3.0