Я буду демонстрировать авторизацию по сертификату на SFTP сервере, но почти всё будет аналогичным и для любых других сервисов, использующих сертификаты для получения доступа. Примеры будут даны на языке C#. Также я покажу как можно сгенерировать и установить сам сертификат.
Генерация сертификата
Первое что нужно сделать, это собственно получить сертификат, с которым можно будет работать. Если у вас уже есть готовый сертификат, можете переходить к следующему разделу.
Для генерации сертификата можно воспользоваться «PuTTY Key Generator», скачать и установить которую можно отсюда https://www.putty.org/
Для генерации достаточно открыть программу, нажать «Generate» и поводить указателем мыши по экрану.
После этого можно сохранить public и private ключи для дальнейшего использования. Public ключ будет размещен на вашем SFTP сервера, о том каким образом это можно сделать в данной статье я писать не буду. Здесь нужно иметь ввиду, что по кнопке «Save private key» будет сохранён ключ с расширением .ppk, этот ключ можно использовать для проверки подключения, например в FileZilla, но нам нужно получить RSA PRIVATE KEY. Сделать это можно выбрав Conversion
– Export 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 клиент.
Выбрать File – Site Manager. Добавить новый сайт, заполнить данные, в качестве Logon Type выбрать “Key file”, указать сам .ppk ключ и нажать Connect
Если все данные верны, тогда вы соединитесь с вашим SFTP сервером.
На этом всё, надеюсь информация будет полезной.
Приятного программирования.