--- openssh-5.0p1/Makefile.in +++ openssh-5.0p1/Makefile.in @@ -62,7 +62,7 @@ TARGETS=ssh$(EXEEXT) sshd$(EXEEXT) ssh-add$(EXEEXT) ssh-keygen$(EXEEXT) ssh-keyscan${EXEEXT} ssh-keysign${EXEEXT} ssh-agent$(EXEEXT) scp$(EXEEXT) ssh-rand-helper${EXEEXT} sftp-server$(EXEEXT) sftp$(EXEEXT) -LIBSSH_OBJS=acss.o authfd.o authfile.o bufaux.o bufbn.o buffer.o \ +LIBSSH_OBJS=acss.o authfd.o authfile.o blacklist.o bufaux.o bufbn.o buffer.o \ canohost.o channels.o cipher.o cipher-acss.o cipher-aes.o \ cipher-bf1.o cipher-ctr.o cipher-3des1.o cleanup.o \ compat.o compress.o crc32.o deattack.o fatal.o hostfile.o \ --- openssh-5.0p1/auth-rh-rsa.c +++ openssh-5.0p1/auth-rh-rsa.c @@ -34,6 +34,7 @@ #include "ssh-gss.h" #endif #include "monitor_wrap.h" +#include "blacklist.h" /* import */ extern ServerOptions options; @@ -48,6 +49,9 @@ if (!auth_rhosts(pw, cuser)) return 0; + if (blacklisted_key(client_host_key, 0)) + return 0; + host_status = check_key_in_hostfiles(pw, client_host_key, chost, _PATH_SSH_SYSTEM_HOSTFILE, options.ignore_user_known_hosts ? NULL : _PATH_SSH_USER_HOSTFILE); --- openssh-5.0p1/auth-rsa.c +++ openssh-5.0p1/auth-rsa.c @@ -47,6 +47,7 @@ #include "monitor_wrap.h" #include "ssh.h" #include "misc.h" +#include "blacklist.h" /* import */ extern ServerOptions options; @@ -265,6 +272,9 @@ "actual %d vs. announced %d.", file, linenum, BN_num_bits(key->rsa->n), bits); + if (blacklisted_key(key, 0)) + continue; + /* We have found the desired key. */ /* * If our options do not allow this key to be used, --- openssh-5.0p1/auth2-hostbased.c +++ openssh-5.0p1/auth2-hostbased.c @@ -47,6 +47,7 @@ #endif #include "monitor_wrap.h" #include "pathnames.h" +#include "blacklist.h" /* import */ extern ServerOptions options; @@ -145,6 +146,9 @@ HostStatus host_status; int len; + if (blacklisted_key(key, 0)) + return 0; + resolvedname = get_canonical_hostname(options.use_dns); ipaddr = get_remote_ipaddr(); --- openssh-5.0p1/auth2-pubkey.c +++ openssh-5.0p1/auth2-pubkey.c @@ -52,6 +52,7 @@ #endif #include "monitor_wrap.h" #include "misc.h" +#include "blacklist.h" /* import */ extern ServerOptions options; @@ -272,6 +273,9 @@ int success; char *file; + if (blacklisted_key(key, 0)) + return 0; + file = authorized_keys_file(pw); success = user_key_allowed2(pw, key, file); xfree(file); new file mode 100644 --- /dev/null +++ openssh-5.0p1/blacklist.c @@ -0,0 +1,267 @@ +/* + * Support for RSA/DSA key blacklisting based on partial fingerprints, + * developed under Openwall Project for Owl - http://www.openwall.com/Owl/ + * + * Copyright (c) 2008 Dmitry V. Levin + * + * 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. + * + * The blacklist encoding was designed by Solar Designer and Dmitry V. Levin. + * No intellectual property rights to the encoding scheme are claimed. + * + * This effort was supported by CivicActions - http://www.civicactions.com + * + * The file size to encode 294,903 of 48-bit fingerprints is just 1.3 MB, + * which corresponds to less than 4.5 bytes per fingerprint. + */ + +#include "includes.h" +#include +#include +#include +#include + +#include "atomicio.h" +#include "blacklist.h" +#include "canohost.h" +#include "log.h" +#include "pathnames.h" +#include "servconf.h" +#include "xmalloc.h" + +extern ServerOptions options; + +typedef struct +{ + /* format version identifier */ + char version[8]; + /* index size, in bits */ + uint8_t index_size; + /* offset size, in bits */ + uint8_t offset_size; + /* record size, in bits */ + uint8_t record_bits; + /* number of records */ + uint8_t records[3]; + /* offset shift */ + uint8_t shift[2]; + +} __attribute__((packed)) blacklist_header; + +static unsigned +c2u(uint8_t c) +{ + return (c >= 'a') ? (c - 'a' + 10) : (c - '0'); +} + +static blacklist_error_t +validate_blacklist(const char *fname, int fd, unsigned *bytes, + unsigned *records, unsigned *shift) +{ + unsigned expected; + struct stat st; + blacklist_header header; + + if (fstat(fd, &st)) { + error("fstat for blacklist file %s failed: %m", fname); + return BLACKLIST_ERROR_ACCESS; + } + + if (atomicio(read, fd, &header, sizeof(header)) != sizeof(header)) { + error("read blacklist file %s header failed: %m", fname); + return BLACKLIST_ERROR_ACCESS; + } + + if (memcmp(header.version, "SSH-FP", 6)) { + error("blacklist file %s has unrecognized format", fname); + return BLACKLIST_ERROR_FORMAT; + } + + if (header.index_size != 16 || header.offset_size != 16 || + memcmp(header.version, "SSH-FP00", 8)) { + error("blacklist file %s has unsupported format", fname); + return BLACKLIST_ERROR_VERSION; + } + + *bytes = (header.record_bits >> 3) - 2; + *records = + (((header.records[0] << 8) + + header.records[1]) << 8) + header.records[2]; + *shift = (header.shift[0] << 8) + header.shift[1]; + + expected = sizeof(header) + 0x20000 + (*records) * (*bytes); + if (st.st_size != expected) { + error("blacklist file %s size mismatch: " + "expected size %u, found size %lu", + fname, expected, (unsigned long) st.st_size); + return BLACKLIST_ERROR_ACCESS; + } + + return BLACKLIST_ERROR_NONE; +} + +static int +expected_offset(uint16_t index, uint16_t shift, unsigned records) +{ + return ((index * (long long) records) >> 16) - shift; +} + +static int +xlseek(const char *fname, int fd, unsigned seek) +{ + if (lseek(fd, seek, SEEK_SET) != seek) { + error("lseek for blacklist file %s failed: %m", fname); + return BLACKLIST_ERROR_ACCESS; + } + return BLACKLIST_ERROR_NONE; +} + +static blacklist_error_t +check(const char *fname, int fd, const char *s) +{ + unsigned bytes, records, shift; + unsigned num, i, j; + int off_start, off_end; + blacklist_error_t rc; + uint16_t index; + /* max number of bytes stored in record_bits, minus two bytes used for index */ + uint8_t buf[(0xff >> 3) - 2]; + + if ((rc = validate_blacklist(fname, fd, &bytes, &records, &shift))) + return rc; + + index = (((((c2u(s[0]) << 4) | c2u(s[1])) << 4) | + c2u(s[2])) << 4) | c2u(s[3]); + if (xlseek(fname, fd, sizeof(blacklist_header) + index * 2)) + return BLACKLIST_ERROR_ACCESS; + + if (atomicio(read, fd, buf, 4) != 4) { + error("read blacklist file %s offsets failed: %m", fname); + return BLACKLIST_ERROR_ACCESS; + } + + off_start = (buf[0] << 8) + buf[1] + + expected_offset(index, shift, records); + if (off_start < 0 || (unsigned) off_start > records) { + error("blacklist file %s off_start overflow [%d] for index %#x", + fname, off_start, index); + return BLACKLIST_ERROR_ACCESS; + } + if (index < 0xffff) { + off_end = (buf[2] << 8) + buf[3] + + expected_offset(index + 1, shift, records); + if (off_end < off_start || (unsigned) off_end > records) { + error("blacklist file %s off_end overflow [%d] for index %#x", + fname, off_end, index); + return BLACKLIST_ERROR_ACCESS; + } + } else + off_end = records; + + if (xlseek(fname, fd, + sizeof(blacklist_header) + 0x20000 + off_start * bytes)) + return BLACKLIST_ERROR_ACCESS; + + num = off_end - off_start; + for (i = 0; i < num; ++i) { + if (atomicio(read, fd, buf, bytes) != bytes) { + error("read blacklist file %s fingerprints failed: %m", + fname); + return BLACKLIST_ERROR_ACCESS; + } + + for (j = 0; j < bytes; ++j) + if (((c2u(s[4 + j * 2]) << 4) | c2u(s[5 + j * 2])) != + buf[j]) + break; + if (j >= bytes) { + debug("blacklisted fingerprint: %s offset=%u, number=%u", + s, off_start, i); + return BLACKLIST_ERROR_ALL; + } + } + + debug("non-blacklisted fingerprint: %s offset=%u, number=%u", + s, off_start, num); + return BLACKLIST_ERROR_NONE; +} + +static blacklist_error_t +blacklisted_fingerprint(const char *hex) +{ + int fd = -1; + blacklist_error_t rc = BLACKLIST_ERROR_ACCESS; + const char *fname = _PATH_BLACKLIST; + char *s, *p; + + debug("Checking fingerprint %s using blacklist file %s", hex, fname); + + s = xstrdup(hex); + for (p = s; *hex; ++hex) + if (*hex != ':') + *p++ = *hex; + *p = '\0'; + + if (strlen(s) != 32 || strlen(s) != strspn(s, "0123456789abcdef")) { + error("%s: invalid fingerprint", s); + goto out; + } + + if ((fd = open(fname, O_RDONLY)) < 0) { + if (ENOENT == errno) { + rc = BLACKLIST_ERROR_MISSING; + verbose("open blacklist file %s failed: %m", fname); + } else + logit("open blacklist file %s failed: %m", fname); + goto out; + } + + rc = check(fname, fd, s); + +out: + close(fd); + xfree(s); + return rc; +} + +int +blacklisted_key(Key *key, int hostkey) +{ + int rc; + const char *text; + char *fp = key_fingerprint(key, SSH_FP_MD5, SSH_FP_HEX); + + switch ((rc = blacklisted_fingerprint(fp))) { + case BLACKLIST_ERROR_NONE: + break; + case BLACKLIST_ERROR_ALL: + text = (options.ignore_blacklist_errors == rc) ? + "Permitted" : "Rejected"; + if (hostkey) + logit("%s blacklisted host key %s", text, fp); + else + logit("%s blacklisted public key %s from %.100s", + text, fp, get_remote_ipaddr()); + break; + default: + if (hostkey) + logit("Unable to check blacklist for host key %s", + fp); + else + logit("Unable to check blacklist for public key %s from %.100s", + fp, get_remote_ipaddr()); + } + + xfree(fp); + return (rc > options.ignore_blacklist_errors); +} new file mode 100644 --- /dev/null +++ openssh-5.0p1/blacklist.h @@ -0,0 +1,37 @@ +/* + * Support for RSA/DSA key blacklisting based on partial fingerprints, + * developed under Openwall Project for Owl - http://www.openwall.com/Owl/ + * + * Copyright (c) 2008 Dmitry V. Levin + * + * 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 BLACKLIST_H_ +#define BLACKLIST_H_ + +#include "key.h" + +int blacklisted_key(Key *, int); + +typedef enum +{ + BLACKLIST_ERROR_NONE = 0, + BLACKLIST_ERROR_MISSING, + BLACKLIST_ERROR_VERSION, + BLACKLIST_ERROR_FORMAT, + BLACKLIST_ERROR_ACCESS, + BLACKLIST_ERROR_ALL +} blacklist_error_t; + +#endif /* BLACKLIST_H_ */ --- openssh-5.0p1/pathnames.h +++ openssh-5.0p1/pathnames.h @@ -43,6 +43,8 @@ /* Backwards compatibility */ #define _PATH_DH_PRIMES SSHDIR "/primes" +#define _PATH_BLACKLIST SSHDIR "/blacklist" + #ifndef _PATH_SSH_PROGRAM #define _PATH_SSH_PROGRAM "/usr/bin/ssh" #endif --- openssh-5.0p1/servconf.c +++ openssh-5.0p1/servconf.c @@ -39,6 +39,7 @@ #include "match.h" #include "channels.h" #include "groupaccess.h" +#include "blacklist.h" static void add_listen_addr(ServerOptions *, char *, u_short); static void add_one_listen_addr(ServerOptions *, char *, u_short); @@ -94,6 +95,7 @@ options->password_authentication = -1; options->kbd_interactive_authentication = -1; options->challenge_response_authentication = -1; + options->ignore_blacklist_errors = -1; options->permit_empty_passwd = -1; options->permit_user_env = -1; options->use_login = -1; @@ -213,6 +217,8 @@ options->kbd_interactive_authentication = 0; if (options->challenge_response_authentication == -1) options->challenge_response_authentication = 1; + if (options->ignore_blacklist_errors == -1) + options->ignore_blacklist_errors = BLACKLIST_ERROR_ALL; //VERSION; if (options->permit_empty_passwd == -1) options->permit_empty_passwd = 0; if (options->permit_user_env == -1) @@ -282,7 +299,7 @@ sListenAddress, sAddressFamily, sPrintMotd, sPrintLastLog, sIgnoreRhosts, sX11Forwarding, sX11DisplayOffset, sX11UseLocalhost, - sStrictModes, sEmptyPasswd, sTCPKeepAlive, + sStrictModes, sIgnoreBlacklistErrors, sEmptyPasswd, sTCPKeepAlive, sPermitUserEnvironment, sUseLogin, sAllowTcpForwarding, sCompression, sAllowUsers, sDenyUsers, sAllowGroups, sDenyGroups, sIgnoreUserKnownHosts, sCiphers, sMacs, sProtocol, sPidFile, @@ -372,6 +390,7 @@ { "x11uselocalhost", sX11UseLocalhost, SSHCFG_ALL }, { "xauthlocation", sXAuthLocation, SSHCFG_GLOBAL }, { "strictmodes", sStrictModes, SSHCFG_GLOBAL }, + { "ignoreblacklisterrors", sIgnoreBlacklistErrors, SSHCFG_GLOBAL }, { "permitemptypasswords", sEmptyPasswd, SSHCFG_GLOBAL }, { "permituserenvironment", sPermitUserEnvironment, SSHCFG_GLOBAL }, { "uselogin", sUseLogin, SSHCFG_GLOBAL }, @@ -923,6 +944,32 @@ intptr = &options->tcp_keep_alive; goto parse_flag; + case sIgnoreBlacklistErrors: + intptr = &options->ignore_blacklist_errors; + arg = strdelim(&cp); + if (!arg || *arg == '\0') + fatal("%s line %d: missing none/missing/version/format/access/all argument.", + filename, linenum); + value = 0; /* silence compiler */ + if (strcmp(arg, "none") == 0) + value = BLACKLIST_ERROR_NONE; + else if (strcmp(arg, "missing") == 0) + value = BLACKLIST_ERROR_MISSING; + else if (strcmp(arg, "version") == 0) + value = BLACKLIST_ERROR_VERSION; + else if (strcmp(arg, "format") == 0) + value = BLACKLIST_ERROR_FORMAT; + else if (strcmp(arg, "access") == 0) + value = BLACKLIST_ERROR_ACCESS; + else if (strcmp(arg, "all") == 0) + value = BLACKLIST_ERROR_ALL; + else + fatal("%s line %d: Bad none/missing/version/format/access/all argument: %s", + filename, linenum, arg); + if (*activep && *intptr == -1) + *intptr = value; + break; + case sEmptyPasswd: intptr = &options->permit_empty_passwd; goto parse_flag; --- openssh-5.0p1/servconf.h +++ openssh-5.0p1/servconf.h @@ -95,6 +95,7 @@ * authentication. */ int kbd_interactive_authentication; /* If true, permit */ int challenge_response_authentication; + int ignore_blacklist_errors; /* none/missing/version/format/access/all */ int permit_empty_passwd; /* If false, do not permit empty * passwords. */ int permit_user_env; /* If true, read ~/.ssh/environment */ --- openssh-5.0p1/sshd.c +++ openssh-5.0p1/sshd.c @@ -118,6 +118,7 @@ #include "monitor_wrap.h" #include "monitor_fdpass.h" #include "version.h" +#include "blacklist.h" #ifdef LIBWRAP #include @@ -1484,6 +1494,11 @@ sensitive_data.host_keys[i] = NULL; continue; } + if (blacklisted_key(key, 1)) { + sensitive_data.host_keys[i] = NULL; + key_free(key); + continue; + } switch (key->type) { case KEY_RSA1: sensitive_data.ssh1_host_key = key; --- openssh-5.0p1/sshd_config.5 +++ openssh-5.0p1/sshd_config.5 @@ -611,6 +611,39 @@ Specifies whether password authentication is allowed. The default is .Dq yes . +.It Cm IgnoreBlacklistErrors +Specifies whether +.Xr sshd 8 +should allow keys recorded in its blacklist of known-compromised keys. +If +.Dq all , +then attempts to authenticate with compromised keys will be logged +but accepted. +If +.Dq access , +then attempts to authenticate with compromised keys will be rejected, +but blacklist file access errors will be ignored. +If +.Dq format , +then attempts to authenticate with compromised keys will be rejected, but +blacklist file access errors due to missing blacklist file or blacklist +file unrecognized format will be ignored. +If +.Dq version , +then attempts to authenticate with compromised keys will be rejected, but +blacklist file access errors due to missing blacklist file or blacklist +file format version mismatch will be ignored. +If +.Dq missing , +then attempts to authenticate with compromised keys will be rejected, +but blacklist file access errors due to missing blacklist file will +be ignored. +If +.Dq none , +then attempts to authenticate with compromised keys, or in case of +any blacklist file access error, will be rejected. +The default is +.Dq version . .It Cm PermitEmptyPasswords When password authentication is allowed, it specifies whether the server allows login to accounts with empty password strings.