File openssh-Add-ssh-keygen-Y-option-sshsig.patch of Package openssh.29886
From 377d5cee13c69e58e0fa513da35d3c0e546d1c32 Mon Sep 17 00:00:00 2001
From: Michal Suchanek <msuchanek@suse.de>
Date: Fri, 11 Nov 2022 08:14:19 +0100
Subject: [PATCH] Add ssh-keygen -Y option (sshsig)
Patch-mainline: V_8_1_P1
Git-commit: 85443f165b4169b2a448b3e24bc1d4dc5b3156a4
OpenBSD-Commit-ID: 304e95381b39c774c8fced7e5328b106a3ff0400
upstream: factor out confirm_overwrite(); ok markus@
Patch-mainline: V_8_1_P1
Git-commit: 2a9c9f7272c1e8665155118fe6536bebdafb6166
OpenBSD-Commit-ID: 2ab568e7114c933346616392579d72be65a4b8fb
upstream: sshsig: lightweight signature and verification ability
for OpenSSH
This adds a simple manual signature scheme to OpenSSH.
Signatures can be made and verified using ssh-keygen -Y sign|verify
Signatures embed the key used to make them. At verification time, this
is matched via principal name against an authorized_keys-like list
of allowed signers.
Mostly by Sebastian Kinne w/ some tweaks by me
ok markus@
Differences on 7.2
- load_identity sshkey_verify have one fewer argument which was NULL
anyway
- RSA key algorithm check is missing -> skipped
- the struct sshkey is apssed as const pointer because 7.2 does not have
private key in-memory encryption
- define SSH_ERR_SIGN_ALG_UNSUPPORTED which is broght in by different
feature upstream
Patch-mainline: V_8_1_P1
Git-commit: d637c4aee6f9b5280c13c020d7653444ac1fcaa5
OpenBSD-Commit-ID: ea4f46ad5a16b27af96e08c4877423918c4253e9
upstream: sshsig tweaks and improvements from and suggested by
Markus
ok markus/me
Patch-mainline: V_8_1_P1
Git-commit: 6d6427d01304d967e58544cf1c71d2b4394c0522
OpenBSD-Regress-ID: 74c0974f2cdae8d9599b9d76a09680bae55d8a8b
upstream: regress test for sshsig; feedback and ok markus@
Patch-mainline: V_8_1_P1
Git-commit: 1a72c0dd89f09754df443c9576dde624a17d7dd0
portability fixes for sshsig
Patch-mainline: V_8_1_P1
Git-commit: b08a6bc1cc7750c6f8a425d1cdbd86552fffc637
oops; missed including the actual file
Changes in v 7.2
- skip rsa1 which is not supported in the new code
Patch-mainline: V_8_1_P1
Git-commit: 0f44e5956c7c816f6600f2a47be4d7bb5a8d711d
OpenBSD-Commit-ID: d125ab720ca71ccf9baf83e08ddc8c12a328597e
upstream: repair typo and editing mishap
Patch-mainline: V_8_1_P1
Git-commit: bab6feb01f9924758ca7129dba708298a53dde5f
OpenBSD-Commit-ID: 0c67600ef04187f98e2912ca57b60c22a8025b7c
upstream: expose allowed_signers options parsing code in header for
fuzzing
rename to make more consistent with philosophically-similar auth
options parsing API.
Patch-mainline: V_8_1_P1
Git-commit: 69159afe24120c97e5ebaf81016c85968afb903e
OpenBSD-Commit-ID: 34d44cb0fb5bdb5fcbc6b02b804e71b20a7a5fc7
upstream: memleak on error path; found by libfuzzer
Patch-mainline: V_8_1_P1
Git-commit: b5a89eec410967d6b712665f8cf0cb632928d74b
OpenBSD-Commit-ID: 80fcc6d52893f80c6de2bedd65353cebfebcfa8f
upstream: make signature format match PROTOCO
Patch-mainline: V_8_1_P1
Git-commit: dc6f81ee94995deb11bbf7e19801022c5f6fd90a
OpenBSD-Commit-ID: 7c5bcf40bed8f4e826230176f4aa353c52aeb698
upstream: ban empty namespace strings for s
Patch-mainline: V_8_1_P1
Git-commit: feff96b7d4c0b99307f0459cbff128aede4a8984
OpenBSD-Commit-ID: ffa3f5a45e09752fc47d9041e2203ee2ec15b24d
upstream: thinko in previous; spotted by Mantas
---
Makefile.in | 4 +-
PROTOCOL.sshsig | 99 ++++++
regress/Makefile | 3 +-
regress/sshsig.sh | 142 +++++++++
ssh-keygen.1 | 119 +++++++
ssh-keygen.c | 359 ++++++++++++++++++++-
ssherr.h | 1 +
sshsig.c | 790 ++++++++++++++++++++++++++++++++++++++++++++++
sshsig.h | 92 ++++++
9 files changed, 1593 insertions(+), 16 deletions(-)
create mode 100644 PROTOCOL.sshsig
create mode 100644 regress/sshsig.sh
create mode 100644 sshsig.c
create mode 100644 sshsig.h
diff --git a/Makefile.in b/Makefile.in
index 1b4be188..cf6e09fd 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -187,8 +187,8 @@ ssh-add$(EXEEXT): $(LIBCOMPAT) libssh.a ssh-add.o
ssh-agent$(EXEEXT): $(LIBCOMPAT) libssh.a ssh-agent.o ssh-pkcs11-client.o
$(LD) -o $@ ssh-agent.o ssh-pkcs11-client.o $(LDFLAGS) -lssh -lopenbsd-compat $(LIBS)
-ssh-keygen$(EXEEXT): $(LIBCOMPAT) libssh.a ssh-keygen.o
- $(LD) -o $@ ssh-keygen.o $(LDFLAGS) -lssh -lopenbsd-compat $(LIBS)
+ssh-keygen$(EXEEXT): $(LIBCOMPAT) libssh.a ssh-keygen.o sshsig.o
+ $(LD) -o $@ ssh-keygen.o sshsig.o $(LDFLAGS) -lssh -lopenbsd-compat $(LIBS)
ssh-keysign$(EXEEXT): $(LIBCOMPAT) libssh.a ssh-keysign.o readconf.o
$(LD) -o $@ ssh-keysign.o readconf.o $(LDFLAGS) -lssh -lopenbsd-compat $(LIBS)
diff --git a/PROTOCOL.sshsig b/PROTOCOL.sshsig
new file mode 100644
index 00000000..720e1f18
--- /dev/null
+++ b/PROTOCOL.sshsig
@@ -0,0 +1,99 @@
+This document describes a lightweight SSH Signature format
+that is compatible with SSH keys and wire formats.
+
+At present, only detached and armored signatures are supported.
+
+1. Armored format
+
+The Armored SSH signatures consist of a header, a base64
+encoded blob, and a footer.
+
+The header is the string "-----BEGIN SSH SIGNATURE-----"
+followed by a newline. The footer is the string
+"-----END SSH SIGNATURE-----" immediately after a newline.
+
+The header MUST be present at the start of every signature.
+Files containing the signature MUST start with the header.
+Likewise, the footer MUST be present at the end of every
+signature.
+
+The base64 encoded blob SHOULD be broken up by newlines
+every 76 characters.
+
+Example:
+
+-----BEGIN SSH SIGNATURE-----
+U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgJKxoLBJBivUPNTUJUSslQTt2hD
+jozKvHarKeN8uYFqgAAAADZm9vAAAAAAAAAFMAAAALc3NoLWVkMjU1MTkAAABAKNC4IEbt
+Tq0Fb56xhtuE1/lK9H9RZJfON4o6hE9R4ZGFX98gy0+fFJ/1d2/RxnZky0Y7GojwrZkrHT
+FgCqVWAQ==
+-----END SSH SIGNATURE-----
+
+2. Blob format
+
+#define MAGIC_PREAMBLE "SSHSIG"
+#define SIG_VERSION 0x01
+
+ byte[6] MAGIC_PREAMBLE
+ uint32 SIG_VERSION
+ string publickey
+ string namespace
+ string reserved
+ string hash_algorithm
+ string signature
+
+The publickey field MUST contain the serialisation of the
+public key used to make the signature using the usual SSH
+encoding rules, i.e RFC4253, RFC5656,
+draft-ietf-curdle-ssh-ed25519-ed448, etc.
+
+Verifiers MUST reject signatures with versions greater than those
+they support.
+
+The purpose of the namespace value is to specify a unambiguous
+interpretation domain for the signature, e.g. file signing.
+This prevents cross-protocol attacks caused by signatures
+intended for one intended domain being accepted in another.
+The namespace value MUST NOT be the empty string.
+
+The reserved value is present to encode future information
+(e.g. tags) into the signature. Implementations should ignore
+the reserved field if it is not empty.
+
+Data to be signed is first hashed with the specified hash_algorithm.
+This is done to limit the amount of data presented to the signature
+operation, which may be of concern if the signing key is held in limited
+or slow hardware or on a remote ssh-agent. The supported hash algorithms
+are "sha256" and "sha512".
+
+The signature itself is made using the SSH signature algorithm and
+encoding rules for the chosen key type. For RSA signatures, the
+signature algorithm must be "rsa-sha2-512" or "rsa-sha2-256" (i.e.
+not the legacy RSA-SHA1 "ssh-rsa").
+
+This blob is encoded as a string using the RFC4243 encoding
+rules and base64 encoded to form the middle part of the
+armored signature.
+
+
+3. Signed Data, of which the signature goes into the blob above
+
+#define MAGIC_PREAMBLE "SSHSIG"
+
+ byte[6] MAGIC_PREAMBLE
+ string namespace
+ string reserved
+ string hash_algorithm
+ string H(message)
+
+The preamble is the six-byte sequence "SSHSIG". It is included to
+ensure that manual signatures can never be confused with any message
+signed during SSH user or host authentication.
+
+The reserved value is present to encode future information
+(e.g. tags) into the signature. Implementations should ignore
+the reserved field if it is not empty.
+
+The data is concatenated and passed to the SSH signing
+function.
+
diff --git a/regress/Makefile b/regress/Makefile
index 451909c1..81744519 100644
--- a/regress/Makefile
+++ b/regress/Makefile
@@ -75,7 +75,8 @@ LTESTS= connect \
keygen-knownhosts \
hostkey-rotate \
principals-command \
- cert-file
+ cert-file \
+ sshsig
# dhgex \
diff --git a/regress/sshsig.sh b/regress/sshsig.sh
new file mode 100644
index 00000000..729beea9
--- /dev/null
+++ b/regress/sshsig.sh
@@ -0,0 +1,142 @@
+# $OpenBSD: sshsig.sh,v 1.1 2019/09/03 08:37:45 djm Exp $
+# Placed in the Public Domain.
+
+tid="sshsig"
+
+DATA2=$OBJ/${DATANAME}.2
+cat ${DATA} ${DATA} > ${DATA2}
+
+rm -f $OBJ/sshsig-*.sig $OBJ/wrong-key* $OBJ/sigca-key*
+
+sig_namespace="test-$$"
+sig_principal="user-$$@example.com"
+
+# Make a "wrong key"
+${SSHKEYGEN} -t ed25519 -f $OBJ/wrong-key -C "wrong trousers, Grommit" -N '' \
+ || fatal "couldn't generate key"
+WRONG=$OBJ/wrong-key.pub
+
+# Make a CA key.
+${SSHKEYGEN} -t ed25519 -f $OBJ/sigca-key -C "CA" -N '' \
+ || fatal "couldn't generate key"
+CA_PRIV=$OBJ/sigca-key
+CA_PUB=$OBJ/sigca-key.pub
+
+SIGNKEYS="$SSH_KEYTYPES"
+verbose "$tid: make certificates"
+for t in $SSH_KEYTYPES ; do
+ [ "$t" != rsa1 ] || continue
+ ${SSHKEYGEN} -q -s $CA_PRIV -z $$ \
+ -I "regress signature key for $USER" \
+ -n $sig_principal $OBJ/${t} || \
+ fatal "couldn't sign ${t}"
+ SIGNKEYS="$SIGNKEYS ${t}-cert.pub"
+done
+
+for t in $SIGNKEYS; do
+ [ "$t" != rsa1 ] || continue
+ verbose "$tid: check signature for $t"
+ keybase=`basename $t .pub`
+ sigfile=${OBJ}/sshsig-${keybase}.sig
+ pubkey=${OBJ}/${keybase}.pub
+
+ ${SSHKEYGEN} -vvv -Y sign -f ${OBJ}/$t -n $sig_namespace \
+ < $DATA > $sigfile 2>/dev/null || fail "sign using $t failed"
+
+ (printf "$sig_principal " ; cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 || \
+ fail "failed signature for $t key"
+
+ (printf "$sig_principal namespaces=\"$sig_namespace,whatever\" ";
+ cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 || \
+ fail "failed signature for $t key w/ limited namespace"
+
+ # Invalid option
+ (printf "$sig_principal octopus " ; cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t key with bad signers option"
+
+ # Wrong key trusted.
+ (printf "$sig_principal " ; cat $WRONG) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t key with wrong key trusted"
+
+ # incorrect data
+ (printf "$sig_principal " ; cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA2 >/dev/null 2>&1 && \
+ fail "passed signature for wrong data with $t key"
+
+ # wrong principal in signers
+ (printf "josef.k@example.com " ; cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t key with wrong principal"
+
+ # wrong namespace
+ (printf "$sig_principal " ; cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n COWS_COWS_COWS \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t key with wrong namespace"
+
+ # namespace excluded by option
+ (printf "$sig_principal namespaces=\"whatever\" " ;
+ cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t key with excluded namespace"
+
+ # Remaining tests are for certificates only.
+ case "$keybase" in
+ *-cert) ;;
+ *) continue ;;
+ esac
+
+ # correct CA key
+ (printf "$sig_principal cert-authority " ;
+ cat $CA_PUB) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 || \
+ fail "failed signature for $t cert"
+
+ # signing key listed as cert-authority
+ (printf "$sig_principal cert-authority" ;
+ cat $pubkey) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature with $t key listed as CA"
+
+ # CA key not flagged cert-authority
+ (printf "$sig_principal " ; cat $CA_PUB) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t cert with CA not marked"
+
+ # mismatch between cert principal and file
+ (printf "josef.k@example.com cert-authority" ;
+ cat $CA_PUB) > $OBJ/allowed_signers
+ ${SSHKEYGEN} -vvv -Y verify -s $sigfile -n $sig_namespace \
+ -I $sig_principal -f $OBJ/allowed_signers \
+ < $DATA >/dev/null 2>&1 && \
+ fail "accepted signature for $t cert with wrong principal"
+done
+
+# XXX test keys in agent.
+# XXX test revocation
+
diff --git a/ssh-keygen.1 b/ssh-keygen.1
index 5b0a049b..97ce9b86 100644
--- a/ssh-keygen.1
+++ b/ssh-keygen.1
@@ -137,6 +137,18 @@
.Fl Q
.Fl f Ar krl_file
.Ar
+.Nm ssh-keygen
+.Fl Y Cm sign
+.Fl f Ar key_file
+.Fl n Ar namespace
+.Ar
+.Nm ssh-keygen
+.Fl Y Cm verify
+.Fl I Ar signer_identity
+.Fl f Ar allowed_keys_file
+.Fl n Ar namespace
+.Fl s Ar signature_file
+.Op Fl r Ar revocation_file
.Ek
.Sh DESCRIPTION
.Nm
@@ -592,6 +604,62 @@ Specify desired generator when testing candidate moduli for DH-GEX.
.It Fl y
This option will read a private
OpenSSH format file and print an OpenSSH public key to stdout.
+.It Fl Y Ar sign
+Cryptographically sign a file or some data using a SSH key.
+When signing,
+.Nm
+accepts zero or more files to sign on the command-line - if no files
+are specified then
+.Nm
+will sign data presented on standard input.
+Signatures are written to the path of the input file with
+.Dq .sig
+appended, or to standard output if the message to be signed was read from
+standard input.
+.Pp
+The key used for signing is specified using the
+.Fl f
+option and may refer to either a private key, or a public key with the private
+half available via
+.Xr ssh-agent 1 .
+An additional signature namespace, used to prevent signature confusion across
+different domains of use (e.g. file signing vs email signing) must be provided
+via the
+.Fl n
+flag.
+Namespaces are arbitrary strings, and may include:
+.Dq file
+for file signing,
+.Dq email
+for email signing.
+For custom uses, it is recommended to use names following a
+NAMESPACE@YOUR.DOMAIN pattern to generate unambiguous namespaces.
+.It Fl Y Ar verify
+Request to verify a signature generated using
+.Nm
+.Fl Y sign
+as described above.
+When verifying a signature,
+.Nm
+accepts a message on standard input and a signature namespace using
+.Fl n .
+A file containing the corresponding signature must also be supplied using the
+.Fl s
+flag, along with the identity of the signer using
+.Fl I
+and a list of allowed signers via the
+.Fl f
+flag.
+The format of the allowed signers file is documented in the
+.Sx ALLOWED SIGNERS
+section below.
+A file containing revoked keys can be passed using the
+.Fl r
+flag. The revocation file may be a KRL or a one-per-line list
+of public keys.
+Successful verification by an authorized signer is signalled by
+.Nm
+returning a zero exit status.
.It Fl z Ar serial_number
Specifies a serial number to be embedded in the certificate to distinguish
this certificate from others from the same CA.
@@ -809,6 +877,57 @@ then
.Nm
will exit with a non-zero exit status.
A zero exit status will only be returned if no key was revoked.
+.Sh ALLOWED SIGNERS
+When verifying signatures,
+.Nm
+uses a simple list of identities and keys to determine whether a signature
+comes from an authorized source.
+This "allowed signers" file uses a format patterned after the
+AUTHORIZED_KEYS FILE FORMAT described in
+.Xr sshd(8) .
+Each line of the file contains the following space-separated fields:
+principals, options, keytype, base64-encoded key.
+Empty lines and lines starting with a
+.Ql #
+are ignored as comments.
+.Pp
+The principals field is a pattern-list (See PATTERNS in
+.Xr ssh_config 5 )
+consisting of one or more comma-separated USER@DOMAIN identity patterns
+that are accepted for signing.
+When verifying, the identity presented via the
+.Fl I option
+must match a principals pattern in order for the corresponding key to be
+considered acceptable for verification.
+.Pp
+The options (if present) consist of comma-separated option specifications.
+No spaces are permitted, except within double quotes.
+The following option specifications are supported (note that option keywords
+are case-insensitive):
+.Bl -tag -width Ds
+.It Cm cert-authority
+Indicates that this key is accepted as a certificate authority (CA) and
+that certificates signed by this CA may be accepted for verification.
+.It Cm namespaces="namespace-list"
+Specifies a pattern-list of namespaces that are accepted for this key.
+If this option is present, the signature namespace embedded in the
+signature object and presented on the verification command-line must
+match the specified list before the key will be considered acceptable.
+.El
+.Pp
+When verifying signatures made by certificates, the expected principal
+name must match both the principals pattern in the allowed signers file and
+the principals embedded in the certificate itself.
+.Pp
+An example allowed signers file:
+.Bd -literal -offset 3n
+# Comments allowed at start of line
+user1@example.com,user2@example.com ssh-rsa AAAAX1...
+# A certificate authority, trusted for all principals in a domain.
+*@example.com cert-authority ssh-ed25519 AAAB4...
+# A key that is accepted only for file signing.
+user2@example.com namespaces="file" ssh-ed25519 AAA41...
+.Ed
.Sh FILES
.Bl -tag -width Ds -compact
.It Pa ~/.ssh/identity
diff --git a/ssh-keygen.c b/ssh-keygen.c
index c2739e2b..c1d4201b 100644
--- a/ssh-keygen.c
+++ b/ssh-keygen.c
@@ -56,6 +56,7 @@
#include "atomicio.h"
#include "krl.h"
#include "digest.h"
+#include "sshsig.h"
#include "fips.h"
@@ -230,6 +231,30 @@ type_bits_valid(int type, const char *name, u_int32_t *bitsp)
#endif
}
+/*
+ * Checks whether a file exists and, if so, asks the user whether they wish
+ * to overwrite it.
+ * Returns nonzero if the file does not already exist or if the user agrees to
+ * overwrite, or zero otherwise.
+ */
+static int
+confirm_overwrite(const char *filename)
+{
+ char yesno[3];
+ struct stat st;
+
+ if (stat(filename, &st) != 0)
+ return 1;
+ printf("%s already exists.\n", filename);
+ printf("Overwrite (y/n)? ");
+ fflush(stdout);
+ if (fgets(yesno, sizeof(yesno), stdin) == NULL)
+ return 0;
+ if (yesno[0] != 'y' && yesno[0] != 'Y')
+ return 0;
+ return 1;
+}
+
static void
ask_filename(struct passwd *pw, const char *prompt)
{
@@ -2280,6 +2305,279 @@ do_check_krl(struct passwd *pw, int argc, char **argv)
exit(ret);
}
+static struct sshkey *
+load_sign_key(const char *keypath, const struct sshkey *pubkey)
+{
+ size_t i, slen, plen = strlen(keypath);
+ char *privpath = xstrdup(keypath);
+ const char *suffixes[] = { "-cert.pub", ".pub", NULL };
+ struct sshkey *ret = NULL, *privkey = NULL;
+ int r;
+
+ /*
+ * If passed a public key filename, then try to locate the correponding
+ * private key. This lets us specify certificates on the command-line
+ * and have ssh-keygen find the appropriate private key.
+ */
+ for (i = 0; suffixes[i]; i++) {
+ slen = strlen(suffixes[i]);
+ if (plen <= slen ||
+ strcmp(privpath + plen - slen, suffixes[i]) != 0)
+ continue;
+ privpath[plen - slen] = '\0';
+ debug("%s: %s looks like a public key, using private key "
+ "path %s instead", __func__, keypath, privpath);
+ }
+ if ((privkey = load_identity(privpath)) == NULL) {
+ error("Couldn't load identity %s", keypath);
+ goto done;
+ }
+ if (!sshkey_equal_public(pubkey, privkey)) {
+ error("Public key %s doesn't match private %s",
+ keypath, privpath);
+ goto done;
+ }
+ if (sshkey_is_cert(pubkey) && !sshkey_is_cert(privkey)) {
+ /*
+ * Graft the certificate onto the private key to make
+ * it capable of signing.
+ */
+ if ((r = sshkey_to_certified(privkey)) != 0) {
+ error("%s: sshkey_to_certified: %s", __func__,
+ ssh_err(r));
+ goto done;
+ }
+ if ((r = sshkey_cert_copy(pubkey, privkey)) != 0) {
+ error("%s: sshkey_cert_copy: %s", __func__, ssh_err(r));
+ goto done;
+ }
+ }
+ /* success */
+ ret = privkey;
+ privkey = NULL;
+ done:
+ sshkey_free(privkey);
+ free(privpath);
+ return ret;
+}
+
+static int
+sign_one(struct sshkey *signkey, const char *filename, int fd,
+ const char *sig_namespace, sshsig_signer *signer, void *signer_ctx)
+{
+ struct sshbuf *sigbuf = NULL, *abuf = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR, wfd = -1, oerrno;
+ char *wfile = NULL;
+ char *asig = NULL;
+
+ if (!quiet) {
+ if (fd == STDIN_FILENO)
+ fprintf(stderr, "Signing data on standard input\n");
+ else
+ fprintf(stderr, "Signing file %s\n", filename);
+ }
+ if ((r = sshsig_sign_fd(signkey, NULL, fd, sig_namespace,
+ &sigbuf, signer, signer_ctx)) != 0) {
+ error("Signing %s failed: %s", filename, ssh_err(r));
+ goto out;
+ }
+ if ((r = sshsig_armor(sigbuf, &abuf)) != 0) {
+ error("%s: sshsig_armor: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ if ((asig = sshbuf_dup_string(abuf)) == NULL) {
+ error("%s: buffer error", __func__);
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+
+ if (fd == STDIN_FILENO) {
+ fputs(asig, stdout);
+ fflush(stdout);
+ } else {
+ xasprintf(&wfile, "%s.sig", filename);
+ if (confirm_overwrite(wfile)) {
+ if ((wfd = open(wfile, O_WRONLY|O_CREAT|O_TRUNC,
+ 0666)) == -1) {
+ oerrno = errno;
+ error("Cannot open %s: %s",
+ wfile, strerror(errno));
+ errno = oerrno;
+ r = SSH_ERR_SYSTEM_ERROR;
+ goto out;
+ }
+ if (atomicio(vwrite, wfd, asig,
+ strlen(asig)) != strlen(asig)) {
+ oerrno = errno;
+ error("Cannot write to %s: %s",
+ wfile, strerror(errno));
+ errno = oerrno;
+ r = SSH_ERR_SYSTEM_ERROR;
+ goto out;
+ }
+ if (!quiet) {
+ fprintf(stderr, "Write signature to %s\n",
+ wfile);
+ }
+ }
+ }
+ /* success */
+ r = 0;
+ out:
+ free(wfile);
+ free(asig);
+ sshbuf_free(abuf);
+ sshbuf_free(sigbuf);
+ if (wfd != -1)
+ close(wfd);
+ return r;
+}
+
+static int
+sign(const char *keypath, const char *sig_namespace, int argc, char **argv)
+{
+ int i, fd = -1, r, ret = -1;
+ int agent_fd = -1;
+ struct sshkey *pubkey = NULL, *privkey = NULL, *signkey = NULL;
+ sshsig_signer *signer = NULL;
+
+ /* Check file arguments. */
+ for (i = 0; i < argc; i++) {
+ if (strcmp(argv[i], "-") != 0)
+ continue;
+ if (i > 0 || argc > 1)
+ fatal("Cannot sign mix of paths and standard input");
+ }
+
+ if ((r = sshkey_load_public(keypath, &pubkey, NULL)) != 0) {
+ error("Couldn't load public key %s: %s", keypath, ssh_err(r));
+ goto done;
+ }
+
+ if ((r = ssh_get_authentication_socket(&agent_fd)) != 0)
+ debug("Couldn't get agent socket: %s", ssh_err(r));
+ else {
+ if ((r = ssh_agent_has_key(agent_fd, pubkey)) == 0)
+ signer = agent_signer;
+ else
+ debug("Couldn't find key in agent: %s", ssh_err(r));
+ }
+
+ if (signer == NULL) {
+ /* Not using agent - try to load private key */
+ if ((privkey = load_sign_key(keypath, pubkey)) == NULL)
+ goto done;
+ signkey = privkey;
+ } else {
+ /* Will use key in agent */
+ signkey = pubkey;
+ }
+
+ if (argc == 0) {
+ if ((r = sign_one(signkey, "(stdin)", STDIN_FILENO,
+ sig_namespace, signer, &agent_fd)) != 0)
+ goto done;
+ } else {
+ for (i = 0; i < argc; i++) {
+ if (strcmp(argv[i], "-") == 0)
+ fd = STDIN_FILENO;
+ else if ((fd = open(argv[i], O_RDONLY)) == -1) {
+ error("Cannot open %s for signing: %s",
+ argv[i], strerror(errno));
+ goto done;
+ }
+ if ((r = sign_one(signkey, argv[i], fd, sig_namespace,
+ signer, &agent_fd)) != 0)
+ goto done;
+ if (fd != STDIN_FILENO)
+ close(fd);
+ fd = -1;
+ }
+ }
+
+ ret = 0;
+done:
+ if (fd != -1 && fd != STDIN_FILENO)
+ close(fd);
+ sshkey_free(pubkey);
+ sshkey_free(privkey);
+ return ret;
+}
+
+static int
+verify(const char *signature, const char *sig_namespace, const char *principal,
+ const char *allowed_keys, const char *revoked_keys)
+{
+ int r, ret = -1, sigfd = -1;
+ struct sshbuf *sigbuf = NULL, *abuf = NULL;
+ struct sshkey *sign_key = NULL;
+ char *fp = NULL;
+
+ if ((abuf = sshbuf_new()) == NULL)
+ fatal("%s: sshbuf_new() failed", __func__);
+
+ if ((sigfd = open(signature, O_RDONLY)) < 0) {
+ error("Couldn't open signature file %s", signature);
+ goto done;
+ }
+
+ if ((r = sshkey_load_file(sigfd, abuf)) != 0) {
+ error("Couldn't read signature file: %s", ssh_err(r));
+ goto done;
+ }
+ if ((r = sshsig_dearmor(abuf, &sigbuf)) != 0) {
+ error("%s: sshsig_armor: %s", __func__, ssh_err(r));
+ return r;
+ }
+ if ((r = sshsig_verify_fd(sigbuf, STDIN_FILENO, sig_namespace,
+ &sign_key)) != 0)
+ goto done; /* sshsig_verify() prints error */
+
+ if ((fp = sshkey_fingerprint(sign_key, fingerprint_hash,
+ SSH_FP_DEFAULT)) == NULL)
+ fatal("%s: sshkey_fingerprint failed", __func__);
+ debug("Valid (unverified) signature from key %s", fp);
+ free(fp);
+ fp = NULL;
+
+ if (revoked_keys != NULL) {
+ if ((r = sshkey_check_revoked(sign_key, revoked_keys)) != 0) {
+ debug3("sshkey_check_revoked failed: %s", ssh_err(r));
+ goto done;
+ }
+ }
+
+ if ((r = sshsig_check_allowed_keys(allowed_keys, sign_key,
+ principal, sig_namespace)) != 0) {
+ debug3("sshsig_check_allowed_keys failed: %s", ssh_err(r));
+ goto done;
+ }
+ /* success */
+ ret = 0;
+done:
+ if (!quiet) {
+ if (ret == 0) {
+ if ((fp = sshkey_fingerprint(sign_key, fingerprint_hash,
+ SSH_FP_DEFAULT)) == NULL) {
+ fatal("%s: sshkey_fingerprint failed",
+ __func__);
+ }
+ printf("Good \"%s\" signature for %s with %s key %s\n",
+ sig_namespace, principal,
+ sshkey_type(sign_key), fp);
+ } else {
+ printf("Could not verify signature.\n");
+ }
+ }
+ if (sigfd != -1)
+ close(sigfd);
+ sshbuf_free(sigbuf);
+ sshbuf_free(abuf);
+ sshkey_free(sign_key);
+ free(fp);
+ return ret;
+}
+
static void
usage(void)
{
@@ -2314,7 +2612,10 @@ usage(void)
" ssh-keygen -A\n"
" ssh-keygen -k -f krl_file [-u] [-s ca_public] [-z version_number]\n"
" file ...\n"
- " ssh-keygen -Q -f krl_file file ...\n");
+ " ssh-keygen -Q -f krl_file file ...\n"
+ " ssh-keygen -Y sign -f sign_key -n namespace\n"
+ " ssh-keygen -Y verify -I signer_identity -s signature_file\n"
+ " -n namespace -f allowed_keys [-r revoked_keys]\n");
exit(1);
}
@@ -2333,6 +2634,7 @@ main(int argc, char **argv)
int gen_all_hostkeys = 0, gen_krl = 0, update_krl = 0, check_krl = 0;
FILE *f;
const char *errstr;
+ char *sign_op = NULL;
#ifdef WITH_OPENSSL
/* Moduli generation/screening */
char out_file[PATH_MAX], *checkpoint = NULL;
@@ -2365,9 +2667,9 @@ main(int argc, char **argv)
if (gethostname(hostname, sizeof(hostname)) < 0)
fatal("gethostname: %s", strerror(errno));
- /* Remaining characters: Ydw */
+ /* Remaining characters: dw */
while ((opt = getopt(argc, argv, "ABHLQUXceghiklopquvxy"
- "C:D:E:F:G:I:J:K:M:N:O:P:R:S:T:V:W:Z:"
+ "C:D:E:F:G:I:J:K:M:N:O:P:R:S:T:V:W:Y:Z:"
"a:b:f:g:j:m:n:r:s:t:z:")) != -1) {
switch (opt) {
case 'A':
@@ -2520,6 +2822,9 @@ main(int argc, char **argv)
case 'V':
parse_cert_times(optarg);
break;
+ case 'Y':
+ sign_op = optarg;
+ break;
case 'z':
errno = 0;
cert_serial = strtoull(optarg, &ep, 10);
@@ -2576,6 +2881,42 @@ main(int argc, char **argv)
argv += optind;
argc -= optind;
+ if (sign_op != NULL) {
+ if (cert_principals == NULL || *cert_principals == '\0') {
+ error("Too few arguments for sign/verify: "
+ "missing namespace");
+ exit(1);
+ }
+ if (strncmp(sign_op, "sign", 4) == 0) {
+ if (!have_identity) {
+ error("Too few arguments for sign: "
+ "missing key");
+ exit(1);
+ }
+ return sign(identity_file, cert_principals, argc, argv);
+ } else if (strncmp(sign_op, "verify", 6) == 0) {
+ if (ca_key_path == NULL) {
+ error("Too few arguments for verify: "
+ "missing signature file");
+ exit(1);
+ }
+ if (!have_identity) {
+ error("Too few arguments for sign: "
+ "missing allowed keys file");
+ exit(1);
+ }
+ if (cert_key_id == NULL) {
+ error("Too few arguments for verify: "
+ "missing principal ID");
+ exit(1);
+ }
+ return verify(ca_key_path, cert_principals,
+ cert_key_id, identity_file, rr_hostname);
+ }
+ usage();
+ /* NOTREACHED */
+ }
+
if (ca_key_path != NULL) {
if (argc < 1 && !gen_krl) {
error("Too few arguments.");
@@ -2738,16 +3079,8 @@ main(int argc, char **argv)
}
}
/* If the file already exists, ask the user to confirm. */
- if (stat(identity_file, &st) >= 0) {
- char yesno[3];
- printf("%s already exists.\n", identity_file);
- printf("Overwrite (y/n)? ");
- fflush(stdout);
- if (fgets(yesno, sizeof(yesno), stdin) == NULL)
- exit(1);
- if (yesno[0] != 'y' && yesno[0] != 'Y')
- exit(1);
- }
+ if (!confirm_overwrite(identity_file))
+ exit(1);
/* Ask for a passphrase (twice). */
if (identity_passphrase)
passphrase1 = xstrdup(identity_passphrase);
diff --git a/ssherr.h b/ssherr.h
index 6f771b4b..9f2c3427 100644
--- a/ssherr.h
+++ b/ssherr.h
@@ -77,6 +77,7 @@
#define SSH_ERR_CONN_TIMEOUT -53
#define SSH_ERR_CONN_CORRUPT -54
#define SSH_ERR_PROTOCOL_ERROR -55
+#define SSH_ERR_SIGN_ALG_UNSUPPORTED -58
/* Translate a numeric error code to a human-readable error string */
const char *ssh_err(int n);
diff --git a/sshsig.c b/sshsig.c
new file mode 100644
index 00000000..9742e55a
--- /dev/null
+++ b/sshsig.c
@@ -0,0 +1,790 @@
+/*
+ * Copyright (c) 2019 Google LLC
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "includes.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+struct bignum_st;
+typedef struct bignum_st BIGNUM;
+
+#include "authfd.h"
+#include "authfile.h"
+#include "log.h"
+#include "misc.h"
+#include "sshbuf.h"
+#include "sshsig.h"
+#include "ssherr.h"
+#include "sshkey.h"
+#include "match.h"
+#include "digest.h"
+
+#define SIG_VERSION 0x01
+#define MAGIC_PREAMBLE "SSHSIG"
+#define MAGIC_PREAMBLE_LEN (sizeof(MAGIC_PREAMBLE) - 1)
+#define BEGIN_SIGNATURE "-----BEGIN SSH SIGNATURE-----\n"
+#define END_SIGNATURE "-----END SSH SIGNATURE-----"
+#define RSA_SIGN_ALG "rsa-sha2-512" /* XXX maybe make configurable */
+#define RSA_SIGN_ALLOWED "rsa-sha2-512,rsa-sha2-256"
+#define HASHALG_DEFAULT "sha512" /* XXX maybe make configurable */
+#define HASHALG_ALLOWED "sha256,sha512"
+
+int
+sshsig_armor(const struct sshbuf *blob, struct sshbuf **out)
+{
+ struct sshbuf *buf = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+ *out = NULL;
+
+ if ((buf = sshbuf_new()) == NULL) {
+ error("%s: sshbuf_new failed", __func__);
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+
+ if ((r = sshbuf_put(buf, BEGIN_SIGNATURE,
+ sizeof(BEGIN_SIGNATURE)-1)) != 0) {
+ error("%s: sshbuf_putf failed: %s", __func__, ssh_err(r));
+ goto out;
+ }
+
+ if ((r = sshbuf_dtob64(blob, buf, 1)) != 0) {
+ error("%s: Couldn't base64 encode signature blob: %s",
+ __func__, ssh_err(r));
+ goto out;
+ }
+
+ if ((r = sshbuf_put(buf, END_SIGNATURE,
+ sizeof(END_SIGNATURE)-1)) != 0 ||
+ (r = sshbuf_put_u8(buf, '\n')) != 0) {
+ error("%s: sshbuf_put failed: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ /* success */
+ *out = buf;
+ buf = NULL; /* transferred */
+ r = 0;
+ out:
+ sshbuf_free(buf);
+ return r;
+}
+
+int
+sshsig_dearmor(struct sshbuf *sig, struct sshbuf **out)
+{
+ int r;
+ size_t eoffset = 0;
+ struct sshbuf *buf = NULL;
+ struct sshbuf *sbuf = NULL;
+ char *b64 = NULL;
+
+ if ((sbuf = sshbuf_fromb(sig)) == NULL) {
+ error("%s: sshbuf_fromb failed", __func__);
+ return SSH_ERR_ALLOC_FAIL;
+ }
+
+ if ((r = sshbuf_cmp(sbuf, 0,
+ BEGIN_SIGNATURE, sizeof(BEGIN_SIGNATURE)-1)) != 0) {
+ error("Couldn't parse signature: missing header");
+ goto done;
+ }
+
+ if ((r = sshbuf_consume(sbuf, sizeof(BEGIN_SIGNATURE)-1)) != 0) {
+ error("%s: sshbuf_consume failed: %s", __func__, ssh_err(r));
+ goto done;
+ }
+
+ if ((r = sshbuf_find(sbuf, 0, "\n" END_SIGNATURE,
+ sizeof("\n" END_SIGNATURE)-1, &eoffset)) != 0) {
+ error("Couldn't parse signature: missing footer");
+ goto done;
+ }
+
+ if ((r = sshbuf_consume_end(sbuf, sshbuf_len(sbuf)-eoffset)) != 0) {
+ error("%s: sshbuf_consume failed: %s", __func__, ssh_err(r));
+ goto done;
+ }
+
+ if ((b64 = sshbuf_dup_string(sbuf)) == NULL) {
+ error("%s: sshbuf_dup_string failed", __func__);
+ r = SSH_ERR_ALLOC_FAIL;
+ goto done;
+ }
+
+ if ((buf = sshbuf_new()) == NULL) {
+ error("%s: sshbuf_new() failed", __func__);
+ r = SSH_ERR_ALLOC_FAIL;
+ goto done;
+ }
+
+ if ((r = sshbuf_b64tod(buf, b64)) != 0) {
+ error("Couldn't decode signature: %s", ssh_err(r));
+ goto done;
+ }
+
+ /* success */
+ *out = buf;
+ r = 0;
+ buf = NULL; /* transferred */
+done:
+ sshbuf_free(buf);
+ sshbuf_free(sbuf);
+ free(b64);
+ return r;
+}
+
+static int
+sshsig_wrap_sign(struct sshkey *key, const char *hashalg,
+ const struct sshbuf *h_message, const char *sig_namespace,
+ struct sshbuf **out, sshsig_signer *signer, void *signer_ctx)
+{
+ int r;
+ size_t slen = 0;
+ u_char *sig = NULL;
+ struct sshbuf *blob = NULL;
+ struct sshbuf *tosign = NULL;
+ const char *sign_alg = NULL;
+
+ if ((tosign = sshbuf_new()) == NULL ||
+ (blob = sshbuf_new()) == NULL) {
+ error("%s: sshbuf_new failed", __func__);
+ r = SSH_ERR_ALLOC_FAIL;
+ goto done;
+ }
+
+ if ((r = sshbuf_put(tosign, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 ||
+ (r = sshbuf_put_cstring(tosign, sig_namespace)) != 0 ||
+ (r = sshbuf_put_string(tosign, NULL, 0)) != 0 || /* reserved */
+ (r = sshbuf_put_cstring(tosign, hashalg)) != 0 ||
+ (r = sshbuf_put_stringb(tosign, h_message)) != 0) {
+ error("Couldn't construct message to sign: %s", ssh_err(r));
+ goto done;
+ }
+
+ /* If using RSA keys then default to a good signature algorithm */
+ if (sshkey_type_plain(key->type) == KEY_RSA)
+ sign_alg = RSA_SIGN_ALG;
+
+ if (signer != NULL) {
+ if ((r = signer(key, &sig, &slen,
+ sshbuf_ptr(tosign), sshbuf_len(tosign),
+ sign_alg, 0, signer_ctx)) != 0) {
+ error("Couldn't sign message: %s", ssh_err(r));
+ goto done;
+ }
+ } else {
+ if ((r = sshkey_sign(key, &sig, &slen,
+ sshbuf_ptr(tosign), sshbuf_len(tosign),
+ sign_alg, 0)) != 0) {
+ error("Couldn't sign message: %s", ssh_err(r));
+ goto done;
+ }
+ }
+
+ if ((r = sshbuf_put(blob, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 ||
+ (r = sshbuf_put_u32(blob, SIG_VERSION)) != 0 ||
+ (r = sshkey_puts(key, blob)) != 0 ||
+ (r = sshbuf_put_cstring(blob, sig_namespace)) != 0 ||
+ (r = sshbuf_put_string(blob, NULL, 0)) != 0 || /* reserved */
+ (r = sshbuf_put_cstring(blob, hashalg)) != 0 ||
+ (r = sshbuf_put_string(blob, sig, slen)) != 0) {
+ error("Couldn't populate blob: %s", ssh_err(r));
+ goto done;
+ }
+
+ *out = blob;
+ blob = NULL;
+ r = 0;
+done:
+ free(sig);
+ sshbuf_free(blob);
+ sshbuf_free(tosign);
+ return r;
+}
+
+/* Check preamble and version. */
+static int
+sshsig_parse_preamble(struct sshbuf *buf)
+{
+ int r = SSH_ERR_INTERNAL_ERROR;
+ uint32_t sversion;
+
+ if ((r = sshbuf_cmp(buf, 0, MAGIC_PREAMBLE, MAGIC_PREAMBLE_LEN)) != 0 ||
+ (r = sshbuf_consume(buf, (sizeof(MAGIC_PREAMBLE)-1))) != 0 ||
+ (r = sshbuf_get_u32(buf, &sversion)) != 0) {
+ error("Couldn't verify signature: invalid format");
+ return r;
+ }
+
+ if (sversion > SIG_VERSION) {
+ error("Signature version %lu is larger than supported "
+ "version %u", (unsigned long)sversion, SIG_VERSION);
+ return SSH_ERR_INVALID_FORMAT;
+ }
+ return 0;
+}
+
+static int
+sshsig_check_hashalg(const char *hashalg)
+{
+ if (hashalg == NULL ||
+ match_pattern_list(hashalg, HASHALG_ALLOWED, 0) == 1)
+ return 0;
+ error("%s: unsupported hash algorithm \"%.100s\"", __func__, hashalg);
+ return SSH_ERR_SIGN_ALG_UNSUPPORTED;
+}
+
+static int
+sshsig_peek_hashalg(struct sshbuf *signature, char **hashalgp)
+{
+ struct sshbuf *buf = NULL;
+ char *hashalg = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+ if (hashalgp != NULL)
+ *hashalgp = NULL;
+ if ((buf = sshbuf_fromb(signature)) == NULL)
+ return SSH_ERR_ALLOC_FAIL;
+ if ((r = sshsig_parse_preamble(buf)) != 0)
+ goto done;
+ if ((r = sshbuf_get_string_direct(buf, NULL, NULL)) != 0 ||
+ (r = sshbuf_get_string_direct(buf, NULL, NULL)) != 0 ||
+ (r = sshbuf_get_string(buf, NULL, NULL)) != 0 ||
+ (r = sshbuf_get_cstring(buf, &hashalg, NULL)) != 0 ||
+ (r = sshbuf_get_string_direct(buf, NULL, NULL)) != 0) {
+ error("Couldn't parse signature blob: %s", ssh_err(r));
+ goto done;
+ }
+
+ /* success */
+ r = 0;
+ *hashalgp = hashalg;
+ hashalg = NULL;
+ done:
+ free(hashalg);
+ sshbuf_free(buf);
+ return r;
+}
+
+static int
+sshsig_wrap_verify(struct sshbuf *signature, const char *hashalg,
+ const struct sshbuf *h_message, const char *expect_namespace,
+ struct sshkey **sign_keyp)
+{
+ int r = SSH_ERR_INTERNAL_ERROR;
+ struct sshbuf *buf = NULL, *toverify = NULL;
+ struct sshkey *key = NULL;
+ const u_char *sig;
+ char *got_namespace = NULL, *sigtype = NULL, *sig_hashalg = NULL;
+ size_t siglen;
+
+ debug("%s: verify message length %zu", __func__, sshbuf_len(h_message));
+ if (sign_keyp != NULL)
+ *sign_keyp = NULL;
+
+ if ((toverify = sshbuf_new()) == NULL) {
+ error("%s: sshbuf_new failed", __func__);
+ r = SSH_ERR_ALLOC_FAIL;
+ goto done;
+ }
+ if ((r = sshbuf_put(toverify, MAGIC_PREAMBLE,
+ MAGIC_PREAMBLE_LEN)) != 0 ||
+ (r = sshbuf_put_cstring(toverify, expect_namespace)) != 0 ||
+ (r = sshbuf_put_string(toverify, NULL, 0)) != 0 || /* reserved */
+ (r = sshbuf_put_cstring(toverify, hashalg)) != 0 ||
+ (r = sshbuf_put_stringb(toverify, h_message)) != 0) {
+ error("Couldn't construct message to verify: %s", ssh_err(r));
+ goto done;
+ }
+
+ if ((r = sshsig_parse_preamble(signature)) != 0)
+ goto done;
+
+ if ((r = sshkey_froms(signature, &key)) != 0 ||
+ (r = sshbuf_get_cstring(signature, &got_namespace, NULL)) != 0 ||
+ (r = sshbuf_get_string(signature, NULL, NULL)) != 0 ||
+ (r = sshbuf_get_cstring(signature, &sig_hashalg, NULL)) != 0 ||
+ (r = sshbuf_get_string_direct(signature, &sig, &siglen)) != 0) {
+ error("Couldn't parse signature blob: %s", ssh_err(r));
+ goto done;
+ }
+
+ if (sshbuf_len(signature) != 0) {
+ error("Signature contains trailing data");
+ r = SSH_ERR_INVALID_FORMAT;
+ goto done;
+ }
+
+ if (strcmp(expect_namespace, got_namespace) != 0) {
+ error("Couldn't verify signature: namespace does not match");
+ debug("%s: expected namespace \"%s\" received \"%s\"",
+ __func__, expect_namespace, got_namespace);
+ r = SSH_ERR_SIGNATURE_INVALID;
+ goto done;
+ }
+ if (strcmp(hashalg, sig_hashalg) != 0) {
+ error("Couldn't verify signature: hash algorithm mismatch");
+ debug("%s: expected algorithm \"%s\" received \"%s\"",
+ __func__, hashalg, sig_hashalg);
+ r = SSH_ERR_SIGNATURE_INVALID;
+ goto done;
+ }
+ if ((r = sshkey_verify(key, sig, siglen, sshbuf_ptr(toverify),
+ sshbuf_len(toverify), 0)) != 0) {
+ error("Signature verification failed: %s", ssh_err(r));
+ goto done;
+ }
+
+ /* success */
+ r = 0;
+ if (sign_keyp != NULL) {
+ *sign_keyp = key;
+ key = NULL; /* transferred */
+ }
+done:
+ free(got_namespace);
+ free(sigtype);
+ free(sig_hashalg);
+ sshbuf_free(buf);
+ sshbuf_free(toverify);
+ sshkey_free(key);
+ return r;
+}
+
+static int
+hash_buffer(const struct sshbuf *m, const char *hashalg, struct sshbuf **bp)
+{
+ char *hex, hash[SSH_DIGEST_MAX_LENGTH];
+ int alg, r = SSH_ERR_INTERNAL_ERROR;
+ struct sshbuf *b = NULL;
+
+ *bp = NULL;
+ memset(hash, 0, sizeof(hash));
+
+ if ((r = sshsig_check_hashalg(hashalg)) != 0)
+ return r;
+ if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) {
+ error("%s: can't look up hash algorithm %s",
+ __func__, hashalg);
+ return SSH_ERR_INTERNAL_ERROR;
+ }
+ if ((r = ssh_digest_buffer(alg, m, hash, sizeof(hash))) != 0) {
+ error("%s: ssh_digest_buffer failed: %s", __func__, ssh_err(r));
+ return r;
+ }
+ if ((hex = tohex(hash, ssh_digest_bytes(alg))) != NULL) {
+ debug3("%s: final hash: %s", __func__, hex);
+ freezero(hex, strlen(hex));
+ }
+ if ((b = sshbuf_new()) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ if ((r = sshbuf_put(b, hash, ssh_digest_bytes(alg))) != 0) {
+ error("%s: sshbuf_put: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ *bp = b;
+ b = NULL; /* transferred */
+ /* success */
+ r = 0;
+ out:
+ sshbuf_free(b);
+ explicit_bzero(hash, sizeof(hash));
+ return 0;
+}
+
+int
+sshsig_signb(struct sshkey *key, const char *hashalg,
+ const struct sshbuf *message, const char *sig_namespace,
+ struct sshbuf **out, sshsig_signer *signer, void *signer_ctx)
+{
+ struct sshbuf *b = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+ if (hashalg == NULL)
+ hashalg = HASHALG_DEFAULT;
+ if (out != NULL)
+ *out = NULL;
+ if ((r = hash_buffer(message, hashalg, &b)) != 0) {
+ error("%s: hash_buffer failed: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ if ((r = sshsig_wrap_sign(key, hashalg, b, sig_namespace, out,
+ signer, signer_ctx)) != 0)
+ goto out;
+ /* success */
+ r = 0;
+ out:
+ sshbuf_free(b);
+ return r;
+}
+
+int
+sshsig_verifyb(struct sshbuf *signature, const struct sshbuf *message,
+ const char *expect_namespace, struct sshkey **sign_keyp)
+{
+ struct sshbuf *b = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR;
+ char *hashalg = NULL;
+
+ if (sign_keyp != NULL)
+ *sign_keyp = NULL;
+
+ if ((r = sshsig_peek_hashalg(signature, &hashalg)) != 0)
+ return r;
+ debug("%s: signature made with hash \"%s\"", __func__, hashalg);
+ if ((r = hash_buffer(message, hashalg, &b)) != 0) {
+ error("%s: hash_buffer failed: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ if ((r = sshsig_wrap_verify(signature, hashalg, b, expect_namespace,
+ sign_keyp)) != 0)
+ goto out;
+ /* success */
+ r = 0;
+ out:
+ sshbuf_free(b);
+ free(hashalg);
+ return r;
+}
+
+static int
+hash_file(int fd, const char *hashalg, struct sshbuf **bp)
+{
+ char *hex, rbuf[8192], hash[SSH_DIGEST_MAX_LENGTH];
+ ssize_t n, total = 0;
+ struct ssh_digest_ctx *ctx;
+ int alg, oerrno, r = SSH_ERR_INTERNAL_ERROR;
+ struct sshbuf *b = NULL;
+
+ *bp = NULL;
+ memset(hash, 0, sizeof(hash));
+
+ if ((r = sshsig_check_hashalg(hashalg)) != 0)
+ return r;
+ if ((alg = ssh_digest_alg_by_name(hashalg)) == -1) {
+ error("%s: can't look up hash algorithm %s",
+ __func__, hashalg);
+ return SSH_ERR_INTERNAL_ERROR;
+ }
+ if ((ctx = ssh_digest_start(alg)) == NULL) {
+ error("%s: ssh_digest_start failed", __func__);
+ return SSH_ERR_INTERNAL_ERROR;
+ }
+ for (;;) {
+ if ((n = read(fd, rbuf, sizeof(rbuf))) == -1) {
+ if (errno == EINTR || errno == EAGAIN)
+ continue;
+ oerrno = errno;
+ error("%s: read: %s", __func__, strerror(errno));
+ ssh_digest_free(ctx);
+ errno = oerrno;
+ r = SSH_ERR_SYSTEM_ERROR;
+ goto out;
+ } else if (n == 0) {
+ debug2("%s: hashed %zu bytes", __func__, total);
+ break; /* EOF */
+ }
+ total += (size_t)n;
+ if ((r = ssh_digest_update(ctx, rbuf, (size_t)n)) != 0) {
+ error("%s: ssh_digest_update: %s",
+ __func__, ssh_err(r));
+ goto out;
+ }
+ }
+ if ((r = ssh_digest_final(ctx, hash, sizeof(hash))) != 0) {
+ error("%s: ssh_digest_final: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ if ((hex = tohex(hash, ssh_digest_bytes(alg))) != NULL) {
+ debug3("%s: final hash: %s", __func__, hex);
+ freezero(hex, strlen(hex));
+ }
+ if ((b = sshbuf_new()) == NULL) {
+ r = SSH_ERR_ALLOC_FAIL;
+ goto out;
+ }
+ if ((r = sshbuf_put(b, hash, ssh_digest_bytes(alg))) != 0) {
+ error("%s: sshbuf_put: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ *bp = b;
+ b = NULL; /* transferred */
+ /* success */
+ r = 0;
+ out:
+ sshbuf_free(b);
+ ssh_digest_free(ctx);
+ explicit_bzero(hash, sizeof(hash));
+ return 0;
+}
+
+int
+sshsig_sign_fd(struct sshkey *key, const char *hashalg,
+ int fd, const char *sig_namespace, struct sshbuf **out,
+ sshsig_signer *signer, void *signer_ctx)
+{
+ struct sshbuf *b = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR;
+
+ if (hashalg == NULL)
+ hashalg = HASHALG_DEFAULT;
+ if (out != NULL)
+ *out = NULL;
+ if ((r = hash_file(fd, hashalg, &b)) != 0) {
+ error("%s: hash_file failed: %s", __func__, ssh_err(r));
+ return r;
+ }
+ if ((r = sshsig_wrap_sign(key, hashalg, b, sig_namespace, out,
+ signer, signer_ctx)) != 0)
+ goto out;
+ /* success */
+ r = 0;
+ out:
+ sshbuf_free(b);
+ return r;
+}
+
+int
+sshsig_verify_fd(struct sshbuf *signature, int fd,
+ const char *expect_namespace, struct sshkey **sign_keyp)
+{
+ struct sshbuf *b = NULL;
+ int r = SSH_ERR_INTERNAL_ERROR;
+ char *hashalg = NULL;
+
+ if (sign_keyp != NULL)
+ *sign_keyp = NULL;
+
+ if ((r = sshsig_peek_hashalg(signature, &hashalg)) != 0)
+ return r;
+ debug("%s: signature made with hash \"%s\"", __func__, hashalg);
+ if ((r = hash_file(fd, hashalg, &b)) != 0) {
+ error("%s: hash_file failed: %s", __func__, ssh_err(r));
+ goto out;
+ }
+ if ((r = sshsig_wrap_verify(signature, hashalg, b, expect_namespace,
+ sign_keyp)) != 0)
+ goto out;
+ /* success */
+ r = 0;
+ out:
+ sshbuf_free(b);
+ free(hashalg);
+ return r;
+}
+
+struct sshsigopt {
+ int ca;
+ char *namespaces;
+};
+
+struct sshsigopt *
+sshsigopt_parse(const char *opts, const char *path, u_long linenum,
+ const char **errstrp)
+{
+ struct sshsigopt *ret;
+ int r;
+ const char *errstr = NULL;
+
+ if ((ret = calloc(1, sizeof(*ret))) == NULL)
+ return NULL;
+ if (opts == NULL || *opts == '\0')
+ return ret; /* Empty options yields empty options :) */
+
+ while (*opts && *opts != ' ' && *opts != '\t') {
+ /* flag options */
+ if ((r = opt_flag("cert-authority", 0, &opts)) != -1) {
+ ret->ca = 1;
+ } else if (opt_match(&opts, "namespaces")) {
+ if (ret->namespaces != NULL) {
+ errstr = "multiple \"namespaces\" clauses";
+ goto fail;
+ }
+ ret->namespaces = opt_dequote(&opts, &errstr);
+ if (ret->namespaces == NULL)
+ goto fail;
+ }
+ /*
+ * Skip the comma, and move to the next option
+ * (or break out if there are no more).
+ */
+ if (*opts == '\0' || *opts == ' ' || *opts == '\t')
+ break; /* End of options. */
+ /* Anything other than a comma is an unknown option */
+ if (*opts != ',') {
+ errstr = "unknown key option";
+ goto fail;
+ }
+ opts++;
+ if (*opts == '\0') {
+ errstr = "unexpected end-of-options";
+ goto fail;
+ }
+ }
+ /* success */
+ return ret;
+ fail:
+ if (errstrp != NULL)
+ *errstrp = errstr;
+ sshsigopt_free(ret);
+ return NULL;
+}
+
+void
+sshsigopt_free(struct sshsigopt *opts)
+{
+ if (opts == NULL)
+ return;
+ free(opts->namespaces);
+ free(opts);
+}
+
+static int
+check_allowed_keys_line(const char *path, u_long linenum, char *line,
+ const struct sshkey *sign_key, const char *principal,
+ const char *sig_namespace)
+{
+ struct sshkey *found_key = NULL;
+ char *cp, *opts = NULL, *identities = NULL;
+ int r, found = 0;
+ const char *reason = NULL;
+ struct sshsigopt *sigopts = NULL;
+
+ if ((found_key = sshkey_new(KEY_UNSPEC)) == NULL) {
+ error("%s: sshkey_new failed", __func__);
+ return SSH_ERR_ALLOC_FAIL;
+ }
+
+ /* format: identity[,identity...] [option[,option...]] key */
+ cp = line;
+ cp = cp + strspn(cp, " \t"); /* skip leading whitespace */
+ if (*cp == '#' || *cp == '\0')
+ goto done;
+ if ((identities = strdelimw(&cp)) == NULL) {
+ error("%s:%lu: invalid line", path, linenum);
+ goto done;
+ }
+ if (match_pattern_list(principal, identities, 0) != 1) {
+ /* principal didn't match */
+ goto done;
+ }
+ debug("%s: %s:%lu: matched principal \"%s\"",
+ __func__, path, linenum, principal);
+
+ if (sshkey_read(found_key, &cp) != 0) {
+ /* no key? Check for options */
+ opts = cp;
+ if (sshkey_advance_past_options(&cp) != 0) {
+ error("%s:%lu: invalid options",
+ path, linenum);
+ goto done;
+ }
+ *cp++ = '\0';
+ skip_space(&cp);
+ if (sshkey_read(found_key, &cp) != 0) {
+ error("%s:%lu: invalid key", path,
+ linenum);
+ goto done;
+ }
+ }
+ debug3("%s:%lu: options %s", path, linenum, opts == NULL ? "" : opts);
+ if ((sigopts = sshsigopt_parse(opts, path, linenum, &reason)) == NULL) {
+ error("%s:%lu: bad options: %s", path, linenum, reason);
+ goto done;
+ }
+
+ /* Check whether options preclude the use of this key */
+ if (sigopts->namespaces != NULL &&
+ match_pattern_list(sig_namespace, sigopts->namespaces, 0) != 1) {
+ error("%s:%lu: key is not permitted for use in signature "
+ "namespace \"%s\"", path, linenum, sig_namespace);
+ goto done;
+ }
+
+ if (!sigopts->ca && sshkey_equal(found_key, sign_key)) {
+ /* Exact match of key */
+ debug("%s:%lu: matched key and principal", path, linenum);
+ /* success */
+ found = 1;
+ } else if (sigopts->ca && sshkey_is_cert(sign_key) &&
+ sshkey_equal_public(sign_key->cert->signature_key, found_key)) {
+ /* Match of certificate's CA key */
+ if ((r = sshkey_cert_check_authority(sign_key, 0, 1,
+ principal, &reason)) != 0) {
+ error("%s:%lu: certificate not authorized: %s",
+ path, linenum, reason);
+ goto done;
+ }
+ debug("%s:%lu: matched certificate CA key", path, linenum);
+ /* success */
+ found = 1;
+ } else {
+ /* Principal matched but key didn't */
+ goto done;
+ }
+ done:
+ sshkey_free(found_key);
+ sshsigopt_free(sigopts);
+ return found ? 0 : SSH_ERR_KEY_NOT_FOUND;
+}
+
+int
+sshsig_check_allowed_keys(const char *path, const struct sshkey *sign_key,
+ const char *principal, const char *sig_namespace)
+{
+ FILE *f = NULL;
+ char *line = NULL;
+ size_t linesize = 0;
+ u_long linenum = 0;
+ int r, oerrno;
+
+ /* Check key and principal against file */
+ if ((f = fopen(path, "r")) == NULL) {
+ oerrno = errno;
+ error("Unable to open allowed keys file \"%s\": %s",
+ path, strerror(errno));
+ errno = oerrno;
+ return SSH_ERR_SYSTEM_ERROR;
+ }
+
+ while (getline(&line, &linesize, f) != -1) {
+ linenum++;
+ r = check_allowed_keys_line(path, linenum, line, sign_key,
+ principal, sig_namespace);
+ free(line);
+ line = NULL;
+ if (r == SSH_ERR_KEY_NOT_FOUND)
+ continue;
+ else if (r == 0) {
+ /* success */
+ fclose(f);
+ return 0;
+ } else
+ break;
+ }
+ /* Either we hit an error parsing or we simply didn't find the key */
+ fclose(f);
+ free(line);
+ return r == 0 ? SSH_ERR_KEY_NOT_FOUND : r;
+}
diff --git a/sshsig.h b/sshsig.h
new file mode 100644
index 00000000..9a10e194
--- /dev/null
+++ b/sshsig.h
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2019 Google LLC
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SSHSIG_H
+#define SSHSIG_H
+
+struct sshbuf;
+struct sshkey;
+struct sshsigopt;
+
+typedef int sshsig_signer(const struct sshkey *, u_char **, size_t *,
+ const u_char *, size_t, const char *, u_int, void *);
+
+/* Buffer-oriented API */
+
+/*
+ * Creates a detached SSH signature for a given buffer.
+ * Returns 0 on success or a negative SSH_ERR_* error code on failure.
+ * out is populated with the detached signature, or NULL on failure.
+ */
+int sshsig_signb(struct sshkey *key, const char *hashalg,
+ const struct sshbuf *message, const char *sig_namespace,
+ struct sshbuf **out, sshsig_signer *signer, void *signer_ctx);
+
+/*
+ * Verifies that a detached signature is valid and optionally returns key
+ * used to sign via argument.
+ * Returns 0 on success or a negative SSH_ERR_* error code on failure.
+ */
+int sshsig_verifyb(struct sshbuf *signature,
+ const struct sshbuf *message, const char *sig_namespace,
+ struct sshkey **sign_keyp);
+
+/* File/FD-oriented API */
+
+/*
+ * Creates a detached SSH signature for a given file.
+ * Returns 0 on success or a negative SSH_ERR_* error code on failure.
+ * out is populated with the detached signature, or NULL on failure.
+ */
+int sshsig_sign_fd(struct sshkey *key, const char *hashalg,
+ int fd, const char *sig_namespace, struct sshbuf **out,
+ sshsig_signer *signer, void *signer_ctx);
+
+/*
+ * Verifies that a detached signature over a file is valid and optionally
+ * returns key used to sign via argument.
+ * Returns 0 on success or a negative SSH_ERR_* error code on failure.
+ */
+int sshsig_verify_fd(struct sshbuf *signature, int fd,
+ const char *sig_namespace, struct sshkey **sign_keyp);
+
+/* Utility functions */
+
+/*
+ * Return a base64 encoded "ASCII armoured" version of a raw signature.
+ */
+int sshsig_armor(const struct sshbuf *blob, struct sshbuf **out);
+
+/*
+ * Decode a base64 encoded armoured signature to a raw signature.
+ */
+int sshsig_dearmor(struct sshbuf *sig, struct sshbuf **out);
+
+/*
+ * Checks whether a particular key/principal/namespace is permitted by
+ * an allowed_keys file. Returns 0 on success.
+ */
+int sshsig_check_allowed_keys(const char *path, const struct sshkey *sign_key,
+ const char *principal, const char *ns);
+
+/* Parse zero or more allowed_keys signature options */
+struct sshsigopt *sshsigopt_parse(const char *opts,
+ const char *path, u_long linenum, const char **errstrp);
+
+/* Free signature options */
+void sshsigopt_free(struct sshsigopt *opts);
+
+#endif /* SSHSIG_H */
--
2.38.0