Skip to content

Commit

Permalink
Merge pull request #5 from AISGorod/custom-IEsiaSigner
Browse files Browse the repository at this point in the history
Custom IEsiaSigner
  • Loading branch information
vladdy-moses committed Dec 30, 2019
2 parents 38cf71c + 77eced6 commit d536dfa
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 24 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# История изменений

## Версия 1.1.0

- Добавлена возможность реализации своего механизма подписи (интерфейс `IEsiaSigner`).
- Переработан пример для демонстрации подписи запросов по ГОСТ 34.10-2012 при помощи openssl.
- Добавлена инструкция по запуску примера на Ubuntu 18.04 и Windows 10 с WSL.
- Указание сертификата в настройках (`options.Certificate`) признано устаревшим, т.к. ЕСИА отказывается от RS256 для подписи запросов и в .Net core нет поддержки ГОСТ по умолчанию.

## Версия 1.0.2

- Интерфейс `IEsiaEnvironment` сделан публичным.
- Реализации интерфейса для тестовой и продукционной среды сделаны публичными, убран модификатор `sealed`.
- Добавлена возможность в настройках подключения ЕСИА указывать собственную реализацию интерфейса `IEsiaEnvironment`.

## Версия 1.0.1

- Добавлена настройка для сохранения токенов и указания схемы входа/выхода.
- Добавлена картинка в NuGet-пакет.
85 changes: 71 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

Данная библиотека добавляет возможность авторизации через госуслуги (ЕСИА) по протоколу OpenID Connect, а также добавляет интерфейс доступа к REST-сервисам ЕСИА.

## [История изменений](CHANGELOG.md)

## Требования

1. AspNetCore не ниже 2.1.
2. Сертификат ИС должен быть RS256 (не ГОСТ).
2. Алгоритм формирования электронной подписи должен быть RS256 (указывается в настройках ИС на [технологическом портале](https://esia.gosuslugi.ru/console/tech/)).

## Подключение

Expand All @@ -18,11 +20,11 @@ services
.AddEsia("Esia", options =>
{
options.Environment = EsiaEnvironmentType.Test;
//options.EnvironmentInstance = ...; - можно использовать свою реализацию.
//options.EnvironmentInstance = ...; // можно использовать свою реализацию.
options.Mnemonic = "TESTSYS";
options.Certificate = () => new X509Certificate2(System.IO.File.ReadAllBytes(@"c:\cert.pfx"), "");
options.Scope = new[] { "fullname", "snils", "email", "mobile" };
});
services.AddSingleton<IEsiaSigner, OpensslEsiaSigner>(); // нужна своя реализация подписи запросов от ИС в ЕСИА
```
3. Также убедитесь, что в _Startup.cs_ есть подключение _HttpContextAccessor_:
```csharp
Expand Down Expand Up @@ -55,30 +57,85 @@ ViewBag.Contacts = contactsJson.ToString(Newtonsoft.Json.Formatting.Indented);

> Пример использования настроек подключения смотрите в проекте `EsiaSample` на стартовой странице.
## Генерация сертификатов (файлы *.pem, *.key и *.pfx)
## Как запустить пример

> Для ОС Windows 10 необходимо установить [Windows Subsystem for Linux](https://docs.microsoft.com/ru-ru/windows/wsl/install-win10) и Ubuntu 18.04 в нём.
> Действия выполняются внутри терминала этой ОС.
Данный раздел показывает, как можно запустить пример работы с ЕСИА на Ubuntu 18.04 (или Windows 10 c WSL).
Такая конфигурация выбрана из-за того, что на Linux намного удобнее включается поддержка ГОСТ для openssl.

Сперва необходимо обновить списки пакетов: `$ sudo apt update`.

Затем устанавливается пакет для поддержки ГОСТ в openssl: `$ sudo apt install libengine-gost-openssl1.1`.

После этого необходимо открыть файл с настройками openssl: `$ sudo nano /etc/ssl/openssl.cnf`.

> Этот раздел больше походит на шпаргалку или мини-инструкцию для генерации сертификатов через _openssl_.
Дописать в начало файла (например, после `oid_section = new_oids`): `openssl_conf = openssl_def`.

Сперва необходимо сгенерировать сертификат с приватным ключом.
Дописать в конец файла:

Воспользуемся утилитой _openssl_ (генерируется сертификат на 10 лет, что небезопасно):
```ini
[openssl_def]
engines = engine_section

[engine_section]
gost = gost_section

[gost_section]
engine_id = gost
dynamic_path = /usr/lib/x86_64-linux-gnu/engines-1.1/gost.so
default_algorithms = ALL
CRYPT_PARAMS = id-Gost28147-89-CryptoPro-A-ParamSet
```
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 3650

Для проверки установки движка gost можно выполнить следующую команду и сравнить результат с представленным ниже:

```bash
$ openssl engine gost -c
(gost) Reference implementation of GOST engine
[gost89, gost89-cnt, gost89-cnt-12, gost89-cbc, grasshopper-ecb, grasshopper-cbc, grasshopper-cfb, grasshopper-ofb, grasshopper-ctr, md_gost94, gost-mac, md_gost12_256, md_gost12_512, gost-mac-12, gost2001, gost-mac, gost2012_256, gost2012_512, gost-mac-12]
```

Данные о стране, городе, имени сертификата можно вбивать любые, они не играют роли для ЕСИА.
Теперь необходимо сгенерировать ключи для ЕСИА при помощи команд:

После успешного выполнения команды файл `cert.pem` будет содержать искомый сертификат.
Именно этот файл необходимо прикладывать к заявками на подключение ИС к ЕСИА.
```bash
$ openssl req -x509 -newkey gost2012_256 -pkeyopt paramset:A -nodes -keyout esia.key -out esia.pem -days 3650
$ openssl pkcs12 -export -out esia.pfx -inkey esia.key -in esia.pem
```

Файл `key.pem` будет содержать зашифрованный закрытый (приватный) ключ.
Данные о стране, городе, имени сертификата можно вбивать любые, они не играют роли для ЕСИА.

Генерация сертификата в формате PKCS#12 (или PFX, это необходимо для .NET) выполняется следующей командой:
Чтобы проверить, что подпись данных в openssl работает, можете использовать следующую команду:

```bash
$ openssl cms -sign -engine gost -inkey esia.key -signer esia.pem <<< '123'
```
openssl pkcs12 -export -out key.pfx -inkey key.pem -in cert.pem

Должен вернуться вывод с огромным base64-текстом, разбитым на несколько строк.

> Для регистрации ключа в ЕСИА на технологический портал требуется загружать файл `.pem`.
Теперь для запуска примера потребуется:

- изменить мнемонику ИС в `~/samples/EsiaSample/Startup.cs`.
- пусть до ключа и сертификата в `~/samples/EsiaSample/OpensslEsiaSigner.cs`.
- установить [.NET Core SDK](https://docs.microsoft.com/ru-ru/dotnet/core/install/linux-package-manager-ubuntu-1804), если он ещё не стоит.
При этом версия SDK должна совпадать с версией netcore в `~/samples/EsiaSample`.
Это необходимо для Razor.

Запуск примера можно проделать следующим образом:

```bash
$ dotnet build
$ dotnet run -p samples/EsiaSample/
```

Веб-сайт для демонстрации работы с ЕСИА будет доступен по адресу https://localhost:5000/.

> Кстати, замечено, что при включенном ГОСТ в openssl не всегда восстанавливаются пакеты NuGet.
> Временно выключить поддержку ГОСТ можно, закомментировав строку, написанную в настройках openssl в начале файла.
## Есть замечания / хочу внести вклад

Создавайте _issue_, предлагайте свои _pull request_-ы.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Linq;
using System.Threading.Tasks;

namespace EsiaSample.Models
namespace EsiaSample
{
/// <summary>
/// Собственное описание настроек подключения к ЕСИА.
Expand Down
57 changes: 57 additions & 0 deletions samples/EsiaSample/OpensslEsiaSigner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using AISGorod.AspNetCore.Authentication.Esia;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

namespace EsiaSample
{
/// <summary>
/// Простейшая обёртка подписи запросов над openssl.
/// </summary>
public class OpensslEsiaSigner : IEsiaSigner
{
private const string KEY_FILE = @"/home/vladdy/test/esia.key";
private const string CRT_FILE = @"/home/vladdy/test/esia.pem";

public string Sign(byte[] data)
{
Process a = new Process();
a.StartInfo.FileName = "openssl";
a.StartInfo.Arguments = $"cms -sign -binary -stream -engine gost -inkey {KEY_FILE} -signer {CRT_FILE} -nodetach -outform pem";

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
a.StartInfo.FileName = "wsl";
a.StartInfo.Arguments = "openssl " + a.StartInfo.Arguments;
}

a.StartInfo.RedirectStandardInput = true;
a.StartInfo.RedirectStandardOutput = true;
a.StartInfo.UseShellExecute = false;

a.Start();
a.StandardInput.Write(Encoding.UTF8.GetString(data)); // просто передавать массив байтов не получается - ломает подпись
a.StandardInput.Close();

StringBuilder resultData = new StringBuilder();
bool isKeyProcessing = false;
while (!a.StandardOutput.EndOfStream)
{
string line = a.StandardOutput.ReadLine();
if (line == "-----BEGIN CMS-----")
{
isKeyProcessing = true;
}
else if (line == "-----END CMS-----")
{
isKeyProcessing = false;
}
else if (isKeyProcessing)
{
resultData.Append(line);
}
}
return resultData.ToString();
}
}
}
4 changes: 2 additions & 2 deletions samples/EsiaSample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ public void ConfigureServices(IServiceCollection services)
.AddEsia("Esia", options =>
{
//options.Environment = EsiaEnvironmentType.Test;
options.EnvironmentInstance = new Models.CustomEsiaEnvironment();
options.EnvironmentInstance = new CustomEsiaEnvironment();
options.Mnemonic = "TESTSYS";
options.Certificate = () => new X509Certificate2(System.IO.File.ReadAllBytes(@"c:\esia.pfx"), "");
options.Scope = new[] { "fullname", "snils", "email", "mobile", "usr_org" };
options.SaveTokens = true;
});
services.AddSingleton<IEsiaSigner, OpensslEsiaSigner>();

services.AddMvc();
}
Expand Down
4 changes: 2 additions & 2 deletions src/AISGorod.AspNetCore.Authentication.Esia.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<Description>Промежуточное ПО для ASP.NET Core для входа пользователей через портал госуслуг (ЕСИА).

ESIA (gosuslugi) identity provider (middleware) for ASP.NET Core based on OpenID Connect.</Description>
<PackageReleaseNotes>IEsiaEnvironment becomes public.</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/AISGorod/AISGorod.AspNetCore.Authentication.Esia/releases/</PackageReleaseNotes>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>1.0.2</Version>
<Version>1.1.0</Version>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>

Expand Down
12 changes: 9 additions & 3 deletions src/EsiaEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
Expand All @@ -22,11 +23,16 @@ sealed class EsiaEvents : OpenIdConnectEvents
{
private EsiaOptions esiaOptions;
private IEsiaEnvironment esiaEnvironment;
private IEsiaSigner esiaSigner;

public EsiaEvents(EsiaOptions esiaOptions, IEsiaEnvironment esiaEnvironment)
public EsiaEvents(
EsiaOptions esiaOptions,
IEsiaEnvironment esiaEnvironment,
IServiceProvider serviceProvider) // TODO add IEsiaSigner directly
{
this.esiaOptions = esiaOptions;
this.esiaEnvironment = esiaEnvironment;
this.esiaSigner = serviceProvider.GetService<IEsiaSigner>(); // TODO add IEsiaSigner directly
}

/// <summary>
Expand All @@ -51,7 +57,7 @@ public override Task RedirectToIdentityProvider(RedirectContext context)
var state = pm.State;

// set clientSecret
pm.ClientSecret = esiaOptions.SignData(scope, timestamp, clientId, state);
pm.ClientSecret = EsiaExtensions.SignData(esiaSigner, esiaOptions, scope, timestamp, clientId, state);

// ok result
return Task.CompletedTask;
Expand Down Expand Up @@ -79,7 +85,7 @@ public override Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext
var state = pm.State;

// set clientSecret
pm.ClientSecret = esiaOptions.SignData(scope, timestamp, clientId, state);
pm.ClientSecret = EsiaExtensions.SignData(esiaSigner, esiaOptions, scope, timestamp, clientId, state);

// ok
return Task.CompletedTask;
Expand Down
15 changes: 14 additions & 1 deletion src/EsiaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,22 @@ static class EsiaExtensions
/// <summary>
/// Подписывает запрос (или вычисляет client_secret запроса).
/// </summary>
public static string SignData(this EsiaOptions options, string scope, string timestamp, string clientId, string state)
internal static string SignData(IEsiaSigner esiaSigner, EsiaOptions options, string scope, string timestamp, string clientId, string state)
{
byte[] signData = Encoding.UTF8.GetBytes(scope + timestamp + clientId + state);
return (esiaSigner != null)
? esiaSigner.Sign(signData)
: _SignDataRSA(options, signData);
}

/// <summary>
/// Подписывает данные по алгоритму RSA (устарело).
/// </summary>
/// <param name="options">Настройки сервиса.</param>
/// <param name="signData">Данные для подписи.</param>
/// <returns>Подпись (для client_secret).</returns>
private static string _SignDataRSA(EsiaOptions options, byte[] signData)
{
using (var certPfx = options.Certificate())
{
var contentInfo = new ContentInfo(signData);
Expand Down
1 change: 1 addition & 0 deletions src/EsiaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class EsiaOptions// : RemoteAuthenticationOptions
/// Функция, возвращающая сертификат ИС.
/// Обязательно должен быть закрытый (приватный) ключ.
/// </summary>
[Obsolete("Please use IEsiaSigner singleton.")]
public Func<X509Certificate2> Certificate { get; set; }

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion src/EsiaRestService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -42,17 +43,20 @@ class EsiaRestService : IEsiaRestService
{
private HttpContext context;
private IEsiaEnvironment esiaEnvironment;
private IEsiaSigner esiaSigner;
private IOptionsMonitor<OpenIdConnectOptions> optionsMonitor;
private EsiaOptions esiaOptions;

public EsiaRestService(
IHttpContextAccessor httpContextAccessor,
IEsiaEnvironment esiaEnvironment,
IServiceProvider serviceProvider, // TODO add IEsiaSigner directly
IOptionsMonitor<OpenIdConnectOptions> optionsMonitor,
EsiaOptions esiaOptions)
{
this.context = httpContextAccessor.HttpContext;
this.esiaEnvironment = esiaEnvironment;
this.esiaSigner = serviceProvider.GetService<IEsiaSigner>(); // TODO add IEsiaSigner directly
this.optionsMonitor = optionsMonitor;
this.esiaOptions = esiaOptions;
}
Expand Down Expand Up @@ -107,7 +111,7 @@ public async Task RefreshTokensAsync()
var clientId = options.ClientId;
var state = Guid.NewGuid().ToString();

var clientSecret = esiaOptions.SignData(scope, timestamp, clientId, state);
var clientSecret = EsiaExtensions.SignData(esiaSigner, esiaOptions, scope, timestamp, clientId, state);

var pairs = new Dictionary<string, string>()
{
Expand Down
19 changes: 19 additions & 0 deletions src/EsiaSigner.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace AISGorod.AspNetCore.Authentication.Esia
{
/// <summary>
/// Интерфейс для подписи данных с помощью ключа ИС.
/// </summary>
public interface IEsiaSigner
{
/// <summary>
/// Подписать последовательность байт при помощи ключа из сертификата ИС.
/// </summary>
/// <param name="data">Данные для подписи.</param>
/// <returns>Откреплённая подпись.</returns>
string Sign(byte[] data);
}
}

0 comments on commit d536dfa

Please sign in to comment.