- djm@cvs.openbsd.org 2013/10/16 02:31:47
     [readconf.c readconf.h roaming_client.c ssh.1 ssh.c ssh_config.5]
     [sshconnect.c sshconnect.h]
     Implement client-side hostname canonicalisation to allow an explicit
     search path of domain suffixes to use to convert unqualified host names
     to fully-qualified ones for host key matching.
     This is particularly useful for host certificates, which would otherwise
     need to list unqualified names alongside fully-qualified ones (and this
     causes a number of problems).
     "looks fine" markus@
diff --git a/readconf.c b/readconf.c
index 9340eff..de8eb7c 100644
--- a/readconf.c
+++ b/readconf.c
@@ -1,4 +1,4 @@
-/* $OpenBSD: readconf.c,v 1.207 2013/10/14 23:28:23 djm Exp $ */
+/* $OpenBSD: readconf.c,v 1.208 2013/10/16 02:31:45 djm Exp $ */
 /*
  * Author: Tatu Ylonen <ylo@cs.hut.fi>
  * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
@@ -144,6 +144,8 @@
 	oTunnel, oTunnelDevice, oLocalCommand, oPermitLocalCommand,
 	oVisualHostKey, oUseRoaming, oZeroKnowledgePasswordAuthentication,
 	oKexAlgorithms, oIPQoS, oRequestTTY, oIgnoreUnknown, oProxyUseFdpass,
+	oCanonicalDomains, oCanonicaliseHostname, oCanonicaliseMaxDots,
+	oCanonicaliseFallbackLocal, oCanonicalisePermittedCNAMEs,
 	oIgnoredUnknownOption, oDeprecated, oUnsupported
 } OpCodes;
 
@@ -257,6 +259,11 @@
 	{ "ipqos", oIPQoS },
 	{ "requesttty", oRequestTTY },
 	{ "proxyusefdpass", oProxyUseFdpass },
+	{ "canonicaldomains", oCanonicalDomains },
+	{ "canonicalisefallbacklocal", oCanonicaliseFallbackLocal },
+	{ "canonicalisehostname", oCanonicaliseHostname },
+	{ "canonicalisemaxdots", oCanonicaliseMaxDots },
+	{ "canonicalisepermittedcnames", oCanonicalisePermittedCNAMEs },
 	{ "ignoreunknown", oIgnoreUnknown },
 
 	{ NULL, oBadOption }
@@ -535,6 +542,34 @@
 	return result;
 }
 
+/* Check and prepare a domain name: removes trailing '.' and lowercases */
+static void
+valid_domain(char *name, const char *filename, int linenum)
+{
+	size_t i, l = strlen(name);
+	u_char c, last = '\0';
+
+	if (l == 0)
+		fatal("%s line %d: empty hostname suffix", filename, linenum);
+	if (!isalpha((u_char)name[0]) && !isdigit((u_char)name[0]))
+		fatal("%s line %d: hostname suffix \"%.100s\" "
+		    "starts with invalid character", filename, linenum, name);
+	for (i = 0; i < l; i++) {
+		c = tolower((u_char)name[i]);
+		name[i] = (char)c;
+		if (last == '.' && c == '.')
+			fatal("%s line %d: hostname suffix \"%.100s\" contains "
+			    "consecutive separators", filename, linenum, name);
+		if (c != '.' && c != '-' && !isalnum(c) &&
+		    c != '_') /* technically invalid, but common */
+			fatal("%s line %d: hostname suffix \"%.100s\" contains "
+			    "invalid characters", filename, linenum, name);
+		last = c;
+	}
+	if (name[l - 1] == '.')
+		name[l - 1] = '\0';
+}
+
 /*
  * Returns the number of the token pointed to by cp or oBadOption.
  */
@@ -609,6 +644,14 @@
 	{ "auto",			REQUEST_TTY_AUTO },
 	{ NULL, -1 }
 };
+static const struct multistate multistate_canonicalisehostname[] = {
+	{ "true",			SSH_CANONICALISE_YES },
+	{ "false",			SSH_CANONICALISE_NO },
+	{ "yes",			SSH_CANONICALISE_YES },
+	{ "no",				SSH_CANONICALISE_NO },
+	{ "always",			SSH_CANONICALISE_ALWAYS },
+	{ NULL, -1 }
+};
 
 /*
  * Processes a single option line as used in the configuration files. This
@@ -628,6 +671,7 @@
 	size_t len;
 	Forward fwd;
 	const struct multistate *multistate_ptr;
+	struct allowed_cname *cname;
 
 	if (activep == NULL) { /* We are processing a command line directive */
 		cmdline = 1;
@@ -1263,6 +1307,62 @@
 		intptr = &options->proxy_use_fdpass;
 		goto parse_flag;
 
+	case oCanonicalDomains:
+		value = options->num_canonical_domains != 0;
+		while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+			valid_domain(arg, filename, linenum);
+			if (!*activep || value)
+				continue;
+			if (options->num_canonical_domains >= MAX_CANON_DOMAINS)
+				fatal("%s line %d: too many hostname suffixes.",
+				    filename, linenum);
+			options->canonical_domains[
+			    options->num_canonical_domains++] = xstrdup(arg);
+		}
+		break;
+
+	case oCanonicalisePermittedCNAMEs:
+		value = options->num_permitted_cnames != 0;
+		while ((arg = strdelim(&s)) != NULL && *arg != '\0') {
+			/* Either '*' for everything or 'list:list' */
+			if (strcmp(arg, "*") == 0)
+				arg2 = arg;
+			else {
+				lowercase(arg);
+				if ((arg2 = strchr(arg, ':')) == NULL ||
+				    arg2[1] == '\0') {
+					fatal("%s line %d: "
+					    "Invalid permitted CNAME \"%s\"",
+					    filename, linenum, arg);
+				}
+				*arg2 = '\0';
+				arg2++;
+			}
+			if (!*activep || value)
+				continue;
+			if (options->num_permitted_cnames >= MAX_CANON_DOMAINS)
+				fatal("%s line %d: too many permitted CNAMEs.",
+				    filename, linenum);
+			cname = options->permitted_cnames +
+			    options->num_permitted_cnames++;
+			cname->source_list = xstrdup(arg);
+			cname->target_list = xstrdup(arg2);
+		}
+		break;
+
+	case oCanonicaliseHostname:
+		intptr = &options->canonicalise_hostname;
+		multistate_ptr = multistate_canonicalisehostname;
+		goto parse_multistate;
+
+	case oCanonicaliseMaxDots:
+		intptr = &options->canonicalise_max_dots;
+		goto parse_int;
+
+	case oCanonicaliseFallbackLocal:
+		intptr = &options->canonicalise_fallback_local;
+		goto parse_flag;
+
 	case oDeprecated:
 		debug("%s line %d: Deprecated option \"%s\"",
 		    filename, linenum, keyword);
@@ -1426,6 +1526,11 @@
 	options->request_tty = -1;
 	options->proxy_use_fdpass = -1;
 	options->ignored_unknown = NULL;
+	options->num_canonical_domains = 0;
+	options->num_permitted_cnames = 0;
+	options->canonicalise_max_dots = -1;
+	options->canonicalise_fallback_local = -1;
+	options->canonicalise_hostname = -1;
 }
 
 /*
@@ -1579,6 +1684,12 @@
 		options->request_tty = REQUEST_TTY_AUTO;
 	if (options->proxy_use_fdpass == -1)
 		options->proxy_use_fdpass = 0;
+	if (options->canonicalise_max_dots == -1)
+		options->canonicalise_max_dots = 1;
+	if (options->canonicalise_fallback_local == -1)
+		options->canonicalise_fallback_local = 1;
+	if (options->canonicalise_hostname == -1)
+		options->canonicalise_hostname = SSH_CANONICALISE_NO;
 #define CLEAR_ON_NONE(v) \
 	do { \
 		if (v != NULL && strcasecmp(v, "none") == 0) { \