Авторизация по сертификату на SFTP

Я буду демонстрировать авторизацию по сертификату на SFTP сервере, но почти всё будет аналогичным и для любых других сервисов, использующих сертификаты для получения доступа. Примеры будут даны на языке C#. Также я покажу как можно сгенерировать и установить сам сертификат.

Генерация сертификата

Первое что нужно сделать, это собственно получить сертификат, с которым можно будет работать. Если у вас уже есть готовый сертификат, можете переходить к следующему разделу.

Для генерации сертификата можно воспользоваться «PuTTY Key Generator», скачать и установить которую можно отсюда https://www.putty.org/

Для генерации достаточно открыть программу, нажать «Generate» и поводить указателем мыши по экрану.

После этого можно сохранить public и private ключи для дальнейшего использования. Public ключ будет размещен на вашем SFTP сервера, о том каким образом это можно сделать в данной статье я писать не буду. Здесь нужно иметь ввиду, что по кнопке «Save private key» будет сохранён ключ с расширением .ppk, этот ключ можно использовать для проверки подключения, например в FileZilla, но нам нужно получить RSA PRIVATE KEY. Сделать это можно выбрав ConversionExport OpenSSH key.

Я сохранил private ключ под именем test.private. Далее необходимо получить .pem сертификат из данного ключа. Для этого можно воспользоваться утилитой OpenSSL.

Для получения сертификата выполним команду

openssl req -new -x509 -days 1825 -key test.private -out test.pem

В процессе у вас попросит заполнить данные для сертификата. Нужно указать данные вашей компании

Параметр –days показывает насколько дней выпускается сертификат, в данном случае 1825 дней – 5 лет.

На мой взгляд, удобнее всего работать с сертификатом в формате .pfx (он же .p12) Получим его выполнив команду:

openssl pkcs12 -export -out MyCert.pfx -in test.pem -inkey test.private

Попросит указать и подтвердить пароль, которым будет защищён сертификат.

Теперь у нас есть сертификат полностью готовый к использованию.

Установка сертификата

Для работы с private кодом, мы можем хранить его где-то в ресурсах приложения, что не очень правильно с точки зрения безопасности, либо если это возможно, использовать хранилище. Я покажу как работать со вторым вариантом.

Выполним установку сертификата:

В качестве места хранения выберем Local Machine

Укажем текущий пароль от сертификата, а также установим опцию «Mark this key as exportable. This will allow you to back up or transport your keys at a later time.» В таком виде ваш сертификат будет содержать private ключ после установки, который можно будет использовать из вашего приложения.

После завершения установки можно проверить корректность размещения:

В данном случае сертификат установился корректно и готов к использованию.

Получение private key из хранилища средствами C#

Для поиска сертификата в хранилище я буду использовать Thumbprint. Посмотреть его значения можно в Details разделе требуемого сертификата.

Код для поиска максимально простой и представляет из себя следующее:

var thumbprint = "170af897569602508cc81d65c18b983ab0ad8cff";
	X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
	store.Open(OpenFlags.ReadOnly);
	var certificates = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, false);
	store.Close();

Фактически, в данном случае ищутся все сертификаты, содержащие данный thumbprint, в том числе не валидные. Если установить true в четвёртой строке, тогда не валидные сертификаты будут проигнорированы. Дальше получаем private key и соединяемся с SFTP сервером:

if (certificates.Count > 0)
{
	var cert = certificates[0];
	if (cert.HasPrivateKey)
	{
		using (var rsa = (RSACryptoServiceProvider)cert.PrivateKey)
		{
			var outputStream = new StringWriter();

			ExportPrivateKey(rsa, outputStream);

			using (var stream = new MemoryStream())
			{
				using (var writer = new StreamWriter(stream))
				{
					writer.Write(outputStream.ToString());
					writer.Flush();
					stream.Position = 0;

					var privateKeyFile = new PrivateKeyFile(stream, null);

					var sftp = new SftpClient("sftp.flash2048.com", 22, "test-user", privateKeyFile);
					sftp.Connect();

				}
			}
		}
	}

}

Код метода ExportPrivateKey я нашёл на просторах сети. Выглядит он следующим образом:

private static void ExportPrivateKey(RSACryptoServiceProvider csp, TextWriter outputStream)
{
	if (csp.PublicOnly) throw new ArgumentException("CSP does not contain a private key", "csp");
	var parameters = csp.ExportParameters(true);
	using (var stream = new MemoryStream())
	{
		var writer = new BinaryWriter(stream);
		writer.Write((byte)0x30); // SEQUENCE
		using (var innerStream = new MemoryStream())
		{
			var innerWriter = new BinaryWriter(innerStream);
			EncodeIntegerBigEndian(innerWriter, new byte[] { 0x00 }); // Version
			EncodeIntegerBigEndian(innerWriter, parameters.Modulus);
			EncodeIntegerBigEndian(innerWriter, parameters.Exponent);
			EncodeIntegerBigEndian(innerWriter, parameters.D);
			EncodeIntegerBigEndian(innerWriter, parameters.P);
			EncodeIntegerBigEndian(innerWriter, parameters.Q);
			EncodeIntegerBigEndian(innerWriter, parameters.DP);
			EncodeIntegerBigEndian(innerWriter, parameters.DQ);
			EncodeIntegerBigEndian(innerWriter, parameters.InverseQ);
			var length = (int)innerStream.Length;
			EncodeLength(writer, length);
			writer.Write(innerStream.GetBuffer(), 0, length);
		}

		var base64 = Convert.ToBase64String(stream.GetBuffer(), 0, (int)stream.Length).ToCharArray();
		outputStream.WriteLine("-----BEGIN RSA PRIVATE KEY-----");
		// Output as Base64 with lines chopped at 64 characters
		for (var i = 0; i < base64.Length; i += 64)
		{
			outputStream.WriteLine(base64, i, Math.Min(64, base64.Length - i));
		}
		outputStream.WriteLine("-----END RSA PRIVATE KEY-----");
	}
}

private static void EncodeLength(BinaryWriter stream, int length)
{
	if (length < 0) throw new ArgumentOutOfRangeException("length", "Length must be non-negative");
	if (length < 0x80)
	{
		// Short form
		stream.Write((byte)length);
	}
	else
	{
		// Long form
		var temp = length;
		var bytesRequired = 0;
		while (temp > 0)
		{
			temp >>= 8;
			bytesRequired++;
		}
		stream.Write((byte)(bytesRequired | 0x80));
		for (var i = bytesRequired - 1; i >= 0; i--)
		{
			stream.Write((byte)(length >> (8 * i) & 0xff));
		}
	}
}

private static void EncodeIntegerBigEndian(BinaryWriter stream, byte[] value, bool forceUnsigned = true)
{
	stream.Write((byte)0x02); // INTEGER
	var prefixZeros = 0;
	for (var i = 0; i < value.Length; i++)
	{
		if (value[i] != 0) break;
		prefixZeros++;
	}
	if (value.Length - prefixZeros == 0)
	{
		EncodeLength(stream, 1);
		stream.Write((byte)0);
	}
	else
	{
		if (forceUnsigned && value[prefixZeros] > 0x7f)
		{
			// Add a prefix zero to force unsigned if the MSB is 1
			EncodeLength(stream, value.Length - prefixZeros + 1);
			stream.Write((byte)0);
		}
		else
		{
			EncodeLength(stream, value.Length - prefixZeros);
		}
		for (var i = prefixZeros; i < value.Length; i++)
		{
			stream.Write(value[i]);
		}
	}
}

Впервые увидев этот код, мне захотелось избавится от строк "-----BEGIN RSA PRIVATE KEY-----" и "-----END RSA PRIVATE KEY-----". Сделав это, я обнаружил, что больше подключение не работает, падая с ошибкой «Invalid private key file.».

Мне стало любопытно, на каком этапе это валидирует, ведь, на мой взгляд, это бесполезные данные.

Открыв код класса PrivateKeyFile в ILSpy я увидел следующее:

private void Open(Stream privateKey, string passPhrase)
    {
        if (privateKey == null)
        {
            throw new ArgumentNullException("privateKey");
        }
        Match match;
        using (StreamReader streamReader = new StreamReader(privateKey))
        {
            string input = streamReader.ReadToEnd();
            match = PrivateKeyRegex.Match(input);
        }
        if (!match.Success)
        {
            throw new SshException("Invalid private key file.");
        }
//

Так что, если вы будете использовать SSH.NET, не вычищайте данные строчки из кода.

Проверка SFTP через FileZilla

Если у вас не получается установить соединения с SFTP сервером, попробуйте сперва проверить корректность работы самого сервера и имеющихся сертификатов. Сделать это можно через FileZilla клиент.

Выбрать FileSite Manager. Добавить новый сайт, заполнить данные, в качестве Logon Type выбрать “Key file”, указать сам .ppk ключ и нажать Connect

Если все данные верны, тогда вы соединитесь с вашим SFTP сервером.

На этом всё, надеюсь информация будет полезной.

Приятного программирования.

Добавить комментарий