Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12-SP5:GA
openssh-askpass-gnome.26950
openssh-Add-ssh-keygen-Y-option-sshsig.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File openssh-Add-ssh-keygen-Y-option-sshsig.patch of Package openssh-askpass-gnome.26950
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
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor