/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
#include
#include
#include
#include
#include
#include
#include "cockpitknownhosts.h"
/* HACK: This file is a hack around the fact that libssh doesn't provide any
* API to check for the presence of a known host key without actually connecting to
* a remote server. We want to gate our outgoing connections based on the contents
* of known hosts so here's a implementation of that until we can get similar functionality
* into libssh.
* SEE: https://red.libssh.org/issues/209
*/
/* match_pattern and match_pattern_list are copied from libssh/match.c */
/*
* Copyright (c) 2000 Markus Friedl. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Returns true if the given string matches the pattern (which may contain ?
* and * as wildcards), and zero if it does not match.
*/
static int
match_pattern (const char *s,
const char *pattern)
{
if (s == NULL || pattern == NULL)
return 0;
for (;;)
{
/* If at end of pattern, accept if also at end of string. */
if (*pattern == '\0')
return (*s == '\0');
if (*pattern == '*')
{
/* Skip the asterisk. */
pattern++;
/* If at end of pattern, accept immediately. */
if (!*pattern)
return 1;
/* If next character in pattern is known, optimize. */
if (*pattern != '?' && *pattern != '*')
{
/*
* Look instances of the next character in
* pattern, and try to match starting from
* those.
*/
for (; *s; s++)
{
if (*s == *pattern && match_pattern (s + 1, pattern + 1))
return 1;
}
/* Failed. */
return 0;
}
/*
* Move ahead one character at a time and try to
* match at each position.
*/
for (; *s; s++)
{
if (match_pattern (s, pattern))
return 1;
}
/* Failed. */
return 0;
}
/*
* There must be at least one more character in the string.
* If we are at the end, fail.
*/
if (!*s)
return 0;
/* Check if the next character of the string is acceptable. */
if (*pattern != '?' && *pattern != *s)
return 0;
/* Move to the next character, both in string and in pattern. */
s++;
pattern++;
}
/* NOTREACHED */
}
/*
* Tries to match the string against the comma-separated sequence of subpatterns
* (each possibly preceded by ! to indicate negation).
* Returns -1 if negation matches, 1 if there is a positive match, 0 if there is
* no match at all.
*/
static
int match_pattern_list (const char *string,
const char *pattern,
unsigned int len,
int dolower)
{
char sub[1024];
int negated;
int got_positive;
unsigned int i, subi;
got_positive = 0;
for (i = 0; i < len;)
{
/* Check if the subpattern is negated. */
if (pattern[i] == '!')
{
negated = 1;
i++;
}
else
{
negated = 0;
}
/*
* Extract the subpattern up to a comma or end. Convert the
* subpattern to lowercase.
*/
for (subi = 0; i < len && subi < sizeof(sub) - 1 && pattern[i] != ','; subi++, i++)
{
sub[subi] = dolower && isupper (pattern[i]) ?
(char)tolower(pattern[i]) : pattern[i];
}
/* If subpattern too long, return failure (no match). */
if (subi >= sizeof(sub) - 1)
return 0;
/* If the subpattern was terminated by a comma, skip the comma. */
if (i < len && pattern[i] == ',')
i++;
/* Null-terminate the subpattern. */
sub[subi] = '\0';
/* Try to match the subpattern against the string. */
if (match_pattern (string, sub))
{
if (negated)
return -1; /* Negative */
else
got_positive = 1; /* Positive */
}
}
/*
* Return success if got a positive match. If there was a negative
* match, we have already returned -1 and never get here.
*/
return got_positive;
}
/* Matches against a openssh hash:
* |1|base64 encoded salt|base64 encoded hash
* hash := HMAC_SHA1(key=salt,data=host)
*/
static gboolean
matches_hashed (const gchar *line,
const gchar *host)
{
GHmac *hmac = NULL;
gchar *copied = NULL;
gchar *marker;
gsize salt_length;
gsize hash_length;
gsize generated_length = 20; // SHA1 length
guchar generated_hash[generated_length];
gboolean ret = FALSE;
if (strncmp (line, "|1|", 3) != 0)
goto out;
copied = g_strdup (line + 3);
marker = strchr(copied, '|');
if (!marker)
goto out;
// Null to seperate salt and hash
*marker = '\0';
// Set marker to hash start
marker++;
if (!g_base64_decode_inplace (copied, &salt_length) || salt_length < 1)
goto out;
if (!g_base64_decode_inplace (marker, &hash_length) || hash_length < 1)
goto out;
// Generate the sha1 hmac
hmac = g_hmac_new (G_CHECKSUM_SHA1, (guchar *)copied, salt_length);
g_hmac_update (hmac, (guchar *)host, strlen (host));
g_hmac_get_digest (hmac, generated_hash, &generated_length);
if (generated_length == hash_length && memcmp (generated_hash, marker, hash_length) == 0)
ret = TRUE;
out:
if (hmac)
g_hmac_unref (hmac);
g_free (copied);
return ret;
}
gboolean
cockpit_is_host_known (const gchar *known_hosts_file,
const gchar *host,
guint port)
{
gchar buffer[4096] = {0};
gchar *ptr;
gchar *hostport = NULL;
FILE *file = g_fopen (known_hosts_file, "r");
gboolean ret = FALSE;
if (!file)
{
if (errno == ENOENT)
g_debug ("known hosts file %s does not exist", known_hosts_file);
else
g_message ("failed to open known hosts file %s: %m", known_hosts_file);
return FALSE;
}
hostport = g_strdup_printf ("[%s]:%d", host, port);
while (fgets (buffer, sizeof (buffer), file))
{
gchar **tokens = NULL;
ptr = strchr (buffer, '\n');
if (ptr)
*ptr = '\0';
ptr = strchr (buffer,'\r');
if (ptr)
*ptr = '\0';
if (buffer[0] == '\0' || buffer[0] == '#')
continue; /* skip empty lines */
tokens = g_strsplit (buffer, " ", -1);
/* it should have 3 or 4 tokens, we aren't strict since all
* we care about is the host */
if (g_strv_length (tokens) == 3 || g_strv_length (tokens) == 4)
{
ret = matches_hashed (tokens[0], hostport);
if (!ret)
ret = match_pattern_list (hostport, tokens[0], strlen(tokens[0]), 1) == 1;
if (!ret)
ret = matches_hashed (tokens[0], host);
if (!ret)
ret = match_pattern_list (host, tokens[0], strlen(tokens[0]), 1) == 1;
}
g_strfreev (tokens);
if (ret)
break;
}
if (file)
fclose(file);
file = NULL;
g_free (hostport);
/* we did not find anything, end of file*/
return ret;
}