Hi, I just created the bug for this patch at this URL: https://bugzilla.mindrot.org/show_bug.cgi?id=2436 Thanks, Meghana From: Damien Miller <djm@xxxxxxxxxxx<mailto:djm@xxxxxxxxxxx>> Date: Wednesday, July 29, 2015 at 8:53 PM To: Meghana Bhat <mebhat@xxxxxxxxxx<mailto:mebhat@xxxxxxxxxx>> Cc: "openssh-unix-dev@xxxxxxxxxxx<mailto:openssh-unix-dev@xxxxxxxxxxx>" <openssh-unix-dev@xxxxxxxxxxx<mailto:openssh-unix-dev@xxxxxxxxxxx>> Subject: Re: [PATCH] ssh: Add option to present certificates on command line Hi, Thanks for this. Could I ask you to create a bug at https://bugzilla.mindrot.org/ and attach your patch there? We're pretty much closed for the 7.0 release ATM but we'll look at it once we're done. I guess something similar for ssh-add would make sense too... -d On Wed, 29 Jul 2015, Meghana Bhat wrote: Allow users to specify certificates to be used for authentication on the command line with the '-z' argument when running ssh. For successful authentication, the key pair associated with the certificate must also be presented during the ssh. Certificates may also be specified in ssh_config as a CertificateFile. This option is meant the address the issue mentioned in the following exchange: http://lists.mindrot.org/pipermail/openssh-unix-dev/2013-September/031629.html Patch developed against 6.9p. --- readconf.c | 48 +++++++++++++++++++ readconf.h | 6 +++ regress/Makefile | 1 + regress/ssh-cert.sh | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++++ ssh.1 | 17 +++++++ ssh.c | 85 +++++++++++++++++++++++++++++++- ssh.h | 7 +++ ssh_config.5 | 33 +++++++++++++ sshconnect2.c | 47 ++++++++++++++++-- 9 files changed, 375 insertions(+), 5 deletions(-) create mode 100644 regress/ssh-cert.sh diff --git a/readconf.c b/readconf.c index f1c860b..b34213d 100644 --- a/readconf.c +++ b/readconf.c @@ -135,6 +135,7 @@ typedef enum { oPasswordAuthentication, oRSAAuthentication, oChallengeResponseAuthentication, oXAuthLocation, oIdentityFile, oHostName, oPort, oCipher, oRemoteForward, oLocalForward, + oCertificateFile, oUser, oEscapeChar, oRhostsRSAAuthentication, oProxyCommand, oGlobalKnownHostsFile, oUserKnownHostsFile, oConnectionAttempts, oBatchMode, oCheckHostIP, oStrictHostKeyChecking, oCompression, @@ -202,6 +203,7 @@ static struct { { "identityfile", oIdentityFile }, { "identityfile2", oIdentityFile }, /* obsolete */ { "identitiesonly", oIdentitiesOnly }, + { "certificatefile", oCertificateFile }, { "hostname", oHostName }, { "hostkeyalias", oHostKeyAlias }, { "proxycommand", oProxyCommand }, @@ -366,6 +368,37 @@ clear_forwardings(Options *options) } void +add_certificate_file(Options *options, const char *dir, const char *filename, + int userprovided) +{ + char *path; + int i; + + if (options->num_certificate_files >= SSH_MAX_CERTIFICATE_FILES) + fatal("Too many certificate files specified (max %d)", + SSH_MAX_CERTIFICATE_FILES); + + if (dir == NULL) /* no dir, filename is absolute */ + path = xstrdup(filename); + else + (void)xasprintf(&path, "%.100s%.100s", dir, filename); + + /* Avoid registering duplicates */ + for (i = 0; i < options->num_certificate_files; i++) { + if (options->certificate_file_userprovided[i] == userprovided && + strcmp(options->certificate_files[i], path) == 0) { + debug2("%s: ignoring duplicate key %s", __func__, path); + free(path); + return; + } + } + + options->certificate_file_userprovided[options->num_certificate_files] = + userprovided; + options->certificate_files[options->num_certificate_files++] = path; +} + +void add_identity_file(Options *options, const char *dir, const char *filename, int userprovided) { @@ -981,6 +1014,20 @@ parse_time: } break; + case oCertificateFile: + arg = strdelim(&s); + if (!arg || *arg == '\0') + fatal("%.200s line %d: Missing argument.", filename, linenum); + if (*activep) { + intptr = &options->num_certificate_files; + if (*intptr >= SSH_MAX_CERTIFICATE_FILES) + fatal("%.200s line %d: Too many identity files specified (max %d).", + filename, linenum, SSH_MAX_CERTIFICATE_FILES); + add_certificate_file(options, NULL, + arg, flags & SSHCONF_USERCONF); + } + break; + case oXAuthLocation: charptr=&options->xauth_location; goto parse_string; @@ -1625,6 +1672,7 @@ initialize_options(Options * options) options->hostkeyalgorithms = NULL; options->protocol = SSH_PROTO_UNKNOWN; options->num_identity_files = 0; + options->num_certificate_files = 0; options->hostname = NULL; options->host_key_alias = NULL; options->proxy_command = NULL; diff --git a/readconf.h b/readconf.h index bb2d552..f839016 100644 --- a/readconf.h +++ b/readconf.h @@ -94,6 +94,11 @@ typedef struct { char *identity_files[SSH_MAX_IDENTITY_FILES]; int identity_file_userprovided[SSH_MAX_IDENTITY_FILES]; struct sshkey *identity_keys[SSH_MAX_IDENTITY_FILES]; + + int num_certificate_files; /* Number of extra certificates for ssh. */ + char *certificate_files[SSH_MAX_CERTIFICATE_FILES]; + int certificate_file_userprovided[SSH_MAX_CERTIFICATE_FILES]; + struct sshkey *certificates[SSH_MAX_CERTIFICATE_FILES]; /* Local TCP/IP forward requests. */ int num_local_forwards; @@ -194,5 +199,6 @@ void dump_client_config(Options *o, const char *host); void add_local_forward(Options *, const struct Forward *); void add_remote_forward(Options *, const struct Forward *); void add_identity_file(Options *, const char *, const char *, int); +void add_certificate_file(Options *, const char *, const char *, int); #endif /* READCONF_H */ diff --git a/regress/Makefile b/regress/Makefile index cba83f4..67455a8 100644 --- a/regress/Makefile +++ b/regress/Makefile @@ -74,6 +74,7 @@ LTESTS= connect \ hostkey-agent \ keygen-knownhosts \ hostkey-rotate \ + ssh-cert \ principals-command diff --git a/regress/ssh-cert.sh b/regress/ssh-cert.sh new file mode 100644 index 0000000..152278b --- /dev/null +++ b/regress/ssh-cert.sh @@ -0,0 +1,136 @@ +# $OpenBSD: multicert.sh,v 1.1 2014/12/22 08:06:03 djm Exp $ +# Placed in the Public Domain. + +tid="ssh with certificates" + +rm -f $OBJ/user_ca_key* $OBJ/user_key* +rm -f $OBJ/cert_user_key* + +# Create a CA key +${SSHKEYGEN} -q -N '' -t ed25519 -f $OBJ/user_ca_key1 ||\ + fatal "ssh-keygen failed" +${SSHKEYGEN} -q -N '' -t ed25519 -f $OBJ/user_ca_key2 ||\ + fatal "ssh-keygen failed" + +# Make some keys and certificates. +${SSHKEYGEN} -q -N '' -t ed25519 -f $OBJ/user_key1 || \ + fatal "ssh-keygen failed" +${SSHKEYGEN} -q -N '' -t ed25519 -f $OBJ/user_key2 || \ + fatal "ssh-keygen failed" +# Move the certificate to a different address to better control +# when it is offered. +${SSHKEYGEN} -q -s $OBJ/user_ca_key1 -I "regress user key for $USER" \ + -z $$ -n ${USER} $OBJ/user_key1 || + fail "couldn't sign user_key1 with user_ca_key1" +mv $OBJ/user_key1-cert.pub $OBJ/cert_user_key1_1.pub +${SSHKEYGEN} -q -s $OBJ/user_ca_key2 -I "regress user key for $USER" \ + -z $$ -n ${USER} $OBJ/user_key1 || + fail "couldn't sign user_key1 with user_ca_key2" +mv $OBJ/user_key1-cert.pub $OBJ/cert_user_key1_2.pub + +trace 'try with identity files' +opts="-F $OBJ/ssh_proxy -oIdentitiesOnly=yes" +opts2="$opts -i $OBJ/user_key1 -i $OBJ/user_key2" +echo "cert-authority $(cat $OBJ/user_ca_key1.pub)" > $OBJ/authorized_keys_$USER + +for p in ${SSH_PROTOCOLS}; do + # Just keys should fail + ${SSH} $opts2 somehost exit 5$p + r=$? + if [ $r -eq 5$p ]; then + fail "ssh succeeded with no certs in protocol $p" + fi + + # Keys with untrusted cert should fail. + opts3="$opts2 -z $OBJ/cert_user_key1_2.pub" + ${SSH} $opts3 somehost exit 5$p + r=$? + if [ $r -eq 5$p ]; then + fail "ssh succeeded with bad cert in protocol $p" + fi + + # Good cert with bad key should fail. + opts3="$opts -i $OBJ/user_key2 -z $OBJ/cert_user_key1_1.pub" + ${SSH} $opts3 somehost exit 5$p + r=$? + if [ $r -eq 5$p ]; then + fail "ssh succeeded with no matching key in protocol $p" + fi + + # Keys with one trusted cert, should succeed. + opts3="$opts2 -z $OBJ/cert_user_key1_1.pub" + ${SSH} $opts3 somehost exit 5$p + r=$? + if [ $r -ne 5$p ]; then + fail "ssh failed with trusted cert and key in protocol $p" + fi + + # Multiple certs and keys, with one trusted cert, should succeed. + opts3="$opts2 -z $OBJ/cert_user_key1_2.pub -z $OBJ/cert_user_key1_1.pub" + ${SSH} $opts3 somehost exit 5$p + r=$? + if [ $r -ne 5$p ]; then + fail "ssh failed with multiple certs in protocol $p" + fi + + #Keys with trusted certificate specified in config options, should succeed. + opts3="$opts2 -oCertificateFile=$OBJ/cert_user_key1_1.pub" + ${SSH} $opts3 somehost exit 5$p + r=$? + if [ $r -ne 5$p ]; then + fail "ssh failed with trusted cert in config in protocol $p" + fi +done + +#next, using an agent in combination with the keys +SSH_AUTH_SOCK=/nonexistent ${SSHADD} -l > /dev/null 2>&1 +if [ $? -ne 2 ]; then + fatal "ssh-add -l did not fail with exit code 2" +fi + +trace "start agent" +eval `${SSHAGENT} -s` > /dev/null +r=$? +if [ $r -ne 0 ]; then + fatal "could not start ssh-agent: exit code $r" +fi + +# add private keys to agent +${SSHADD} -k $OBJ/user_key2 > /dev/null 2>&1 +if [ $? -ne 0 ]; then + fatal "ssh-add did not succeed with exit code 0" +fi +${SSHADD} -k $OBJ/user_key1 > /dev/null 2>&1 +if [ $? -ne 0 ]; then + fatal "ssh-add did not succeed with exit code 0" +fi + +# try ssh with the agent and certificates +# note: ssh agent only uses certificates in protocol 2 +opts="-F $OBJ/ssh_proxy" +# with no certificates, shoud fail +${SSH} -2 $opts somehost exit 52 +if [ $? -eq 52 ]; then + fail "ssh connect with agent in protocol 2 succeeded with no cert" +fi + +#with an untrusted certificate, should fail +opts="$opts -z $OBJ/cert_user_key1_2.pub" +${SSH} -2 $opts somehost exit 52 +if [ $? -eq 52 ]; then + fail "ssh connect with agent in protocol 2 succeeded with bad cert" +fi + +#with an additional trusted certificate, should succeed +opts="$opts -z $OBJ/cert_user_key1_1.pub" +${SSH} -2 $opts somehost exit 52 +if [ $? -ne 52 ]; then + fail "ssh connect with agent in protocol 2 failed with good cert" +fi + +trace "kill agent" +${SSHAGENT} -k > /dev/null + +#cleanup +rm -f $OBJ/user_ca_key* $OBJ/user_key* +rm -f $OBJ/cert_user_key* diff --git a/ssh.1 b/ssh.1 index 2ea0a20..76a9459 100644 --- a/ssh.1 +++ b/ssh.1 @@ -63,6 +63,7 @@ .Op Fl S Ar ctl_path .Op Fl W Ar host : Ns Ar port .Op Fl w Ar local_tun Ns Op : Ns Ar remote_tun +.Op Fl z Ar certificate_file .Oo Ar user Ns @ Oc Ns Ar hostname .Op Ar command .Ek @@ -468,6 +469,7 @@ For full details of the options listed below, and their possible values, see .It CanonicalizeHostname .It CanonicalizeMaxDots .It CanonicalizePermittedCNAMEs +.It CertificateFile .It ChallengeResponseAuthentication .It CheckHostIP .It Cipher @@ -768,6 +770,21 @@ Send log information using the .Xr syslog 3 system module. By default this information is sent to stderr. +.It Fl z Ar certificate_file +Selects a file from which certificate information is loaded for public +key authentication. For the certificate to be signed, the private key +corresponding to +.Ar certificate_file +must also be provided for authentication, whether through +.Xr ssh_agent 1 . +or through an +.Ar identity_file +specified on the command line or in configuration files. +Certificate files may also be specified on a per-host basis in +the configuration file. It is possible to have multiple +.Fl z +options (and multiple certificates specified in +configuration files). .El .Pp .Nm diff --git a/ssh.c b/ssh.c index 3239108..e01790a 100644 --- a/ssh.c +++ b/ssh.c @@ -207,7 +207,8 @@ usage(void) " [-O ctl_cmd] [-o option] [-p port]\n" " [-Q cipher | cipher-auth | mac | kex | key]\n" " [-R address] [-S ctl_path] [-W host:port]\n" -" [-w local_tun[:remote_tun]] [user@]hostname [command]\n" +" [-w local_tun[:remote_tun]] [-z certificate_file]\n" +" [user@]hostname [command]\n" ); exit(255); } @@ -215,6 +216,7 @@ usage(void) static int ssh_session(void); static int ssh_session2(void); static void load_public_identity_files(void); +static void load_certificate_files(void); static void main_sigchld_handler(int); /* from muxclient.c */ @@ -595,7 +597,7 @@ main(int ac, char **av) again: while ((opt = getopt(ac, av, "1246ab:c:e:fgi:kl:m:no:p:qstvx" - "ACD:E:F:GI:KL:MNO:PQ:R:S:TVw:W:XYy")) != -1) { + "ACD:E:F:GI:KL:MNO:PQ:R:S:TVw:W:XYyz:")) != -1) { switch (opt) { case '1': options.protocol = SSH_PROTO_1; @@ -906,6 +908,9 @@ main(int ac, char **av) case 'F': config = optarg; break; + case 'z': + add_certificate_file(&options, NULL, optarg, 1); + break; default: usage(); } @@ -1013,6 +1018,9 @@ main(int ac, char **av) options.hostname = xstrdup(host); } + /* If the user has specified certificate(s), load it now. */ + load_certificate_files(); + /* If canonicalization requested then try to apply it */ lowercase(host); if (options.canonicalize_hostname != SSH_CANONICALISE_NO) @@ -1353,6 +1361,13 @@ main(int ac, char **av) } } + for (i = 0; i < options.num_certificate_files; i++) { + free(options.certificate_files[i]); + options.certificate_files[i] = NULL; + } + + + exit_status = compat20 ? ssh_session2() : ssh_session(); packet_close(); @@ -1938,6 +1953,72 @@ ssh_session2(void) options.escape_char : SSH_ESCAPECHAR_NONE, id); } +/* Load certificate file(s) specified in options. */ +static void +load_certificate_files(void) +{ + char *filename, *cp, thishost[NI_MAXHOST]; + char *pwdir = NULL, *pwname = NULL; + struct passwd *pw; + int i, n_ids; + struct sshkey *cert; + char *certificate_files[SSH_MAX_CERTIFICATE_FILES]; + struct sshkey *certificates[SSH_MAX_CERTIFICATE_FILES]; + + n_ids = 0; + memset(certificate_files, 0, sizeof(certificate_files)); + memset(certificates, 0, sizeof(certificates)); + + if ((pw = getpwuid(original_real_uid)) == NULL) + fatal("load_certificate_files: getpwuid failed"); + pwname = xstrdup(pw->pw_name); + pwdir = xstrdup(pw->pw_dir); + if (gethostname(thishost, sizeof(thishost)) == -1) + fatal("load_certificate_files: gethostname: %s", + strerror(errno)); + + if (options.num_certificate_files > SSH_MAX_CERTIFICATE_FILES) + fatal("load_certificate_files: too many certificates"); + for (i = 0; i < options.num_certificate_files; i++) { + cp = tilde_expand_filename(options.certificate_files[i], + original_real_uid); + filename = percent_expand(cp, "d", pwdir, + "u", pwname, "l", thishost, "h", host, + "r", options.user, (char *)NULL); + free(cp); + + cert = key_load_public(filename, NULL); + debug("certificate file %s type %d", filename, + cert ? cert->type : -1); + free(options.certificate_files[i]); + if (cert == NULL) { + free(filename); + continue; + } + if (!key_is_cert(cert)) { + debug("%s: key %s type %s is not a certificate", + __func__, filename, key_type(cert)); + key_free(cert); + free(filename); + continue; + } + + certificate_files[n_ids] = filename; + certificates[n_ids] = cert; + ++n_ids; + } + options.num_certificate_files = n_ids; + memcpy(options.certificate_files, certificate_files, sizeof(certificate_files)); + memcpy(options.certificates, certificates, sizeof(certificates)); + + explicit_bzero(pwname, strlen(pwname)); + free(pwname); + explicit_bzero(pwdir, strlen(pwdir)); + free(pwdir); +} + + + static void load_public_identity_files(void) { diff --git a/ssh.h b/ssh.h index 4f8da5c..8fb7ba3 100644 --- a/ssh.h +++ b/ssh.h @@ -19,6 +19,13 @@ #define SSH_DEFAULT_PORT 22 /* + * Maximum number of certificate files that can be specified + * in configuration files or on the command line. + */ +#define SSH_MAX_CERTIFICATE_FILES 100 + + +/* * Maximum number of RSA authentication identity files that can be specified * in configuration files or on the command line. */ diff --git a/ssh_config.5 b/ssh_config.5 index e514398..17741b7 100644 --- a/ssh_config.5 +++ b/ssh_config.5 @@ -325,6 +325,34 @@ to be canonicalized to names in the or .Dq *.c.example.com domains. +.It Cm CertificateFile +Specifies a file from which the user's certificate is read. +A corresponding private key must be provided separately in order +to use this certificate. +.Xr ssh 1 +will attempt to use private keys provided as identity files +or in the agent for such authentication. +.Pp +The file name may use the tilde +syntax to refer to a user's home directory or one of the following +escape characters: +.Ql %d +(local user's home directory), +.Ql %u +(local user name), +.Ql %l +(local host name), +.Ql %h +(remote host name) or +.Ql %r +(remote user name). +.Pp +It is possible to have multiple certificate files specified in +configuration files; these certificates will be tried in sequence. +Multiple +.Cm CertificateFile +directives will add to the list of certificates used for +authentication. .It Cm ChallengeResponseAuthentication Specifies whether to use challenge-response authentication. The argument to this keyword must be @@ -911,6 +939,11 @@ differs from that of other configuration directives). may be used in conjunction with .Cm IdentitiesOnly to select which identities in an agent are offered during authentication. +.Cm IdentityFile +may also be used in conjunction with +.Cm CertificateFile +in order to provide any certificate also needed for authentication with +the identity. .It Cm IgnoreUnknown Specifies a pattern-list of unknown options to be ignored if they are encountered in configuration parsing. diff --git a/sshconnect2.c b/sshconnect2.c index 34dbf9a..fb24b5e 100644 --- a/sshconnect2.c +++ b/sshconnect2.c @@ -1016,6 +1016,7 @@ sign_and_send_pubkey(Authctxt *authctxt, Identity *id) u_int skip = 0; int ret = -1; int have_sig = 1; + int i; char *fp; if ((fp = sshkey_fingerprint(id->key, options.fingerprint_hash, @@ -1053,6 +1054,33 @@ sign_and_send_pubkey(Authctxt *authctxt, Identity *id) } buffer_put_string(&b, blob, bloblen); + /* If the key is an input certificate, sign its private key instead. + * If no such private key exists, return failure and continue with + * other methods of authentication. + * Else, just continue with the normal signing process. */ + if (key_is_cert(id->key)) { + for (i = 0; i < options.num_certificate_files; i++) { + if (key_equal(id->key, options.certificates[i])) { + Identity *id2; + int matched = 0; + TAILQ_FOREACH(id2, &authctxt->keys, next) { + if (sshkey_equal_public(id->key, id2->key) && + id->key->type != id2->key->type) { + id = id2; + matched = 1; + break; + } + } + if (!matched) { + free(blob); + buffer_free(&b); + return 0; + } + break; + } + } + } + /* generate signature */ ret = identity_sign(id, &signature, &slen, buffer_ptr(&b), buffer_len(&b), datafellows); @@ -1189,9 +1217,11 @@ load_identity_file(char *filename, int userprovided) /* * try keys in the following order: - * 1. agent keys that are found in the config file - * 2. other agent keys - * 3. keys that are only listed in the config file + * 1. certificates listed in the config file + * 2. other input certificates + * 3. agent keys that are found in the config file + * 4. other agent keys + * 5. keys that are only listed in the config file */ static void pubkey_prepare(Authctxt *authctxt) @@ -1245,6 +1275,17 @@ pubkey_prepare(Authctxt *authctxt) free(id); } } + /* list of certificates specified by user */ + for (i = 0; i < options.num_certificate_files; i++) { + key = options.certificates[i]; + if (!key_is_cert(key)) + continue; + id = xcalloc(1, sizeof(*id)); + id->key = key; + id->filename = xstrdup(options.certificate_files[i]); + id->userprovided = options.certificate_file_userprovided[i]; + TAILQ_INSERT_TAIL(preferred, id, next); + } /* list of keys supported by the agent */ if ((r = ssh_get_authentication_socket(&agent_fd)) != 0) { if (r != SSH_ERR_AGENT_NOT_PRESENT) -- 1.9.1 _______________________________________________ openssh-unix-dev mailing list openssh-unix-dev@xxxxxxxxxxx<mailto:openssh-unix-dev@xxxxxxxxxxx> https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev _______________________________________________ openssh-unix-dev mailing list openssh-unix-dev@xxxxxxxxxxx https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev